commit 8f22ddf339611e1770b3335ff9eb16bde2034bd5 Author: Zhongwei Li Date: Sat Nov 29 18:26:08 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..f639eff --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "betty-framework", + "description": "Betty Framework is RiskExec's system for structured, auditable AI-assisted engineering.\nWhere Claude Code provides the runtime, Betty adds methodology, orchestration, and governance—\nturning raw agent capability into a repeatable, enterprise-grade engineering discipline.\n", + "version": "1.0.0", + "author": { + "name": "RiskExec", + "email": "platform@riskexec.com", + "url": "https://github.com/epieczko/betty" + }, + "skills": [ + "./skills" + ], + "agents": [ + "./agents" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a25917b --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# betty-framework + +Betty Framework is RiskExec's system for structured, auditable AI-assisted engineering. +Where Claude Code provides the runtime, Betty adds methodology, orchestration, and governance— +turning raw agent capability into a repeatable, enterprise-grade engineering discipline. diff --git a/agents/README.md b/agents/README.md new file mode 100644 index 0000000..1c96999 --- /dev/null +++ b/agents/README.md @@ -0,0 +1,203 @@ +# Betty Framework Agents + +## ⚙️ **Integration Note: Claude Code Plugin System** + +**Betty agents are Claude Code plugins.** You do not invoke agents via standalone CLI commands (`betty` or direct Python scripts). Instead: + +- **Claude Code serves as the execution environment** for all agent invocation +- Each agent is registered through its `agent.yaml` manifest +- Agents become automatically discoverable and executable through Claude Code's natural language interface +- All routing, validation, and execution is handled by Claude Code via MCP (Model Context Protocol) + +**No separate installation step is needed** beyond plugin registration in your Claude Code environment. + +--- + +This directory contains agent manifests for the Betty Framework. + +## What are Agents? + +Agents are intelligent orchestrators that compose skills with reasoning, context awareness, and error recovery. Unlike workflows (which follow fixed sequential steps) or skills (which execute atomic operations), agents can: + +- **Reason** about requirements and choose appropriate strategies +- **Iterate** based on feedback and validation results +- **Recover** from errors with intelligent retry logic +- **Adapt** their approach based on context + +## Directory Structure + +Each agent has its own directory containing: +``` +agents/ +├── / +│ ├── agent.yaml # Agent manifest (required) +│ ├── README.md # Documentation (auto-generated) +│ └── tests/ # Agent behavior tests (optional) +│ └── test_agent.py +``` + +## Creating an Agent + +### Using meta.agent (Recommended) + +**Via Claude Code:** +``` +"Use meta.agent to create a my.agent that does [description], +with capabilities [list], using skills [skill.one, skill.two], +and iterative reasoning mode" +``` + +**Direct execution (development/testing):** +```bash +cat > /tmp/my_agent.md <<'EOF' +# Name: my.agent +# Purpose: What your agent does +# Capabilities: First capability, Second capability +# Skills: skill.one, skill.two +# Reasoning: iterative +EOF +python agents/meta.agent/meta_agent.py /tmp/my_agent.md +``` + +### Manual Creation + +1. Create agent directory: + ```bash + mkdir -p agents/my.agent + ``` + +2. Create agent manifest (`agents/my.agent/agent.yaml`): + ```yaml + name: my.agent + version: 0.1.0 + description: "What your agent does" + + capabilities: + - First capability + - Second capability + + skills_available: + - skill.one + - skill.two + + reasoning_mode: iterative # or oneshot + + status: draft + ``` + +3. Validate and register: + + **Via Claude Code:** + ``` + "Use agent.define to validate agents/my.agent/agent.yaml" + ``` + + **Direct execution (development/testing):** + ```bash + python skills/agent.define/agent_define.py agents/my.agent/agent.yaml + ``` + +## Agent Manifest Schema + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Unique identifier (e.g., `api.designer`) | +| `version` | string | Semantic version (e.g., `0.1.0`) | +| `description` | string | Human-readable purpose statement | +| `capabilities` | array[string] | List of what the agent can do | +| `skills_available` | array[string] | Skills the agent can orchestrate | +| `reasoning_mode` | enum | `iterative` or `oneshot` | + +### Optional Fields + +| Field | Type | Description | +|-------|------|-------------| +| `status` | enum | `draft`, `active`, `deprecated`, `archived` | +| `context_requirements` | object | Structured context the agent needs | +| `workflow_pattern` | string | Narrative description of reasoning process | +| `example_task` | string | Concrete usage example | +| `error_handling` | object | Retry strategies and failure handling | +| `output` | object | Expected success/failure outputs | +| `tags` | array[string] | Categorization tags | +| `dependencies` | array[string] | Other agents or schemas | + +## Reasoning Modes + +### Iterative +Agent can retry with feedback, refine based on errors, and improve incrementally. + +**Use for:** +- Validation loops (API design with validation feedback) +- Refinement tasks (code optimization) +- Error correction (fixing compilation errors) + +**Example:** +```yaml +reasoning_mode: iterative + +error_handling: + max_retries: 3 + on_validation_failure: "Analyze errors, refine spec, retry" +``` + +### Oneshot +Agent executes once without retry. + +**Use for:** +- Analysis and reporting (compatibility checks) +- Deterministic transformations (code generation) +- Tasks where retry doesn't help (documentation) + +**Example:** +```yaml +reasoning_mode: oneshot + +output: + success: + - Analysis report + failure: + - Error details +``` + +## Example Agents + +See the documentation for example agent manifests: +- [API Designer](../docs/agent-schema-reference.md#example-iterative-refinement-agent) - Iterative API design +- [Compliance Checker](../docs/agent-schema-reference.md#example-multi-domain-agent) - Multi-domain compliance + +## Validation + +All agent manifests are automatically validated for: +- Required fields presence +- Name format (`^[a-z][a-z0-9._-]*$`) +- Version format (semantic versioning) +- Reasoning mode enum (`iterative` or `oneshot`) +- Skill references (all skills must exist in skill registry) + +## Registry + +Validated agents are registered in `/registry/agents.json`: +```json +{ + "registry_version": "1.0.0", + "generated_at": "2025-10-23T00:00:00Z", + "agents": [ + { + "name": "api.designer", + "version": "0.1.0", + "description": "Design RESTful APIs...", + "reasoning_mode": "iterative", + "skills_available": ["api.define", "api.validate"], + "status": "draft" + } + ] +} +``` + +## See Also + +- [Agent Schema Reference](../docs/agent-schema-reference.md) - Complete field reference +- [Betty Architecture](../docs/betty-architecture.md) - Five-layer architecture +- [Agent Implementation Plan](../docs/agent-define-implementation-plan.md) - Implementation details diff --git a/agents/ai.orchestrator/README.md b/agents/ai.orchestrator/README.md new file mode 100644 index 0000000..cb8d45c --- /dev/null +++ b/agents/ai.orchestrator/README.md @@ -0,0 +1,45 @@ +# Ai.Orchestrator Agent + +Orchestrates AI/ML workflows including model training, evaluation, and deployment + +## Purpose + +This orchestrator agent coordinates complex ai workflows by composing and sequencing multiple skills. It handles the complete lifecycle from planning through execution and validation. + +## Capabilities + +- Coordinate meta-agent creation and composition +- Manage skill and agent generation workflows +- Orchestrate AI-powered automation +- Handle agent compatibility and optimization +- Coordinate marketplace publishing + +## Available Skills + +- `agent.compose` +- `agent.define` +- `agent.run` +- `generate.docs` +- `generate.marketplace` +- `meta.compatibility` + +## Usage + +This agent uses iterative reasoning to: +1. Analyze requirements +2. Plan execution steps +3. Coordinate skill execution +4. Validate results +5. Handle errors and retries + +## Status + +**Generated**: Auto-generated from taxonomy gap analysis + +## Next Steps + +- [ ] Review and refine capabilities +- [ ] Test with real workflows +- [ ] Add domain-specific examples +- [ ] Integrate with existing agents +- [ ] Document best practices diff --git a/agents/ai.orchestrator/agent.yaml b/agents/ai.orchestrator/agent.yaml new file mode 100644 index 0000000..14655a9 --- /dev/null +++ b/agents/ai.orchestrator/agent.yaml @@ -0,0 +1,55 @@ +name: ai.orchestrator +version: 0.1.0 +description: Orchestrates AI/ML workflows including model training, evaluation, and + deployment +capabilities: +- Coordinate meta-agent creation and composition +- Manage skill and agent generation workflows +- Orchestrate AI-powered automation +- Handle agent compatibility and optimization +- Coordinate marketplace publishing +skills_available: +- agent.compose +- agent.define +- agent.run +- generate.docs +- generate.marketplace +- meta.compatibility +reasoning_mode: iterative +tags: +- ai +- orchestration +- meta +- automation +workflow_pattern: '1. Analyze incoming request and requirements + + 2. Identify relevant ai skills and workflows + + 3. Compose multi-step execution plan + + 4. Execute skills in coordinated sequence + + 5. Validate intermediate results + + 6. Handle errors and retry as needed + + 7. Return comprehensive results' +example_task: "Input: \"Complete ai workflow from start to finish\"\n\nAgent will:\n\ + 1. Break down the task into stages\n2. Select appropriate skills for each stage\n\ + 3. Execute create \u2192 validate \u2192 review \u2192 publish lifecycle\n4. Monitor\ + \ progress and handle failures\n5. Generate comprehensive reports" +error_handling: + timeout_seconds: 300 + retry_strategy: exponential_backoff + max_retries: 3 +output: + success: + - Ai workflow results + - Execution logs and metrics + - Validation reports + - Generated artifacts + failure: + - Error details and stack traces + - Partial results (if available) + - Remediation suggestions +status: generated diff --git a/agents/api.analyzer/README.md b/agents/api.analyzer/README.md new file mode 100644 index 0000000..43be3d5 --- /dev/null +++ b/agents/api.analyzer/README.md @@ -0,0 +1,302 @@ +# api.analyzer Agent + +## Purpose + +**api.analyzer** is a specialized agent that analyzes API specifications for backward compatibility and breaking changes between versions. + +This agent provides detailed compatibility reports, identifies breaking vs non-breaking changes, and suggests migration paths for consumers when breaking changes are unavoidable. + +## Behavior + +- **Reasoning Mode**: `oneshot` – The agent executes once without retries, as compatibility analysis is deterministic +- **Capabilities**: + - Detect breaking changes between API versions + - Generate detailed compatibility reports + - Identify removed or modified endpoints + - Suggest migration paths for breaking changes + - Validate API evolution best practices + +## Skills Used + +The agent has access to the following skills: + +| Skill | Purpose | +|-------|---------| +| `api.compatibility` | Compares two API spec versions and detects breaking changes | +| `api.validate` | Validates individual specs for well-formedness | + +## Workflow Pattern + +The agent follows this straightforward pattern: + +``` +1. Load old and new API specifications +2. Run comprehensive compatibility analysis +3. Categorize changes as breaking or non-breaking +4. Generate detailed report with migration recommendations +5. Return results (no retry needed) +``` + +## Manifest Fields (Quick Reference) + +```yaml +name: api.analyzer +version: 0.1.0 +reasoning_mode: oneshot +skills_available: + - api.compatibility + - api.validate +status: draft +``` + +## Usage + +This agent is invoked through a command or workflow: + +### Via Slash Command + +```bash +# Assuming /api-compatibility command is registered to use this agent +/api-compatibility specs/user-service-v1.yaml specs/user-service-v2.yaml +``` + +### Via Workflow + +Include the agent in a workflow YAML: + +```yaml +# workflows/check_api_compatibility.yaml +steps: + - agent: api.analyzer + input: + old_spec_path: "specs/user-service-v1.0.0.yaml" + new_spec_path: "specs/user-service-v2.0.0.yaml" + fail_on_breaking: true +``` + +## Context Requirements + +The agent expects the following context: + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `old_spec_path` | string | Path to the previous/old API specification | `"specs/api-v1.0.0.yaml"` | +| `new_spec_path` | string | Path to the new/updated API specification | `"specs/api-v2.0.0.yaml"` | +| `fail_on_breaking` | boolean | Whether to fail (exit non-zero) if breaking changes detected | `true` or `false` | + +## Example Task + +**Input**: +``` +"Compare user-service v1.0.0 with v2.0.0 for breaking changes" +``` + +**Agent execution**: + +1. **Load both specifications**: + - Old: `specs/user-service-v1.0.0.openapi.yaml` + - New: `specs/user-service-v2.0.0.openapi.yaml` + +2. **Analyze endpoint changes**: + - ✅ **Added**: `GET /users/{id}/preferences` (non-breaking) + - ❌ **Removed**: `DELETE /users/{id}/avatar` (breaking) + - ⚠️ **Modified**: `POST /users` now requires additional field `email_verified` (breaking) + +3. **Check for breaking schema changes**: + - ❌ Removed property: `User.phoneNumber` (breaking) + - ✅ Added optional property: `User.preferences` (non-breaking) + - ❌ Changed property type: `User.age` from `integer` to `string` (breaking) + +4. **Identify parameter or response format changes**: + - ❌ Query parameter `filter` changed from optional to required in `GET /users` (breaking) + - ✅ Response includes new optional field `User.last_login` (non-breaking) + +5. **Generate compatibility report**: + ```json + { + "compatible": false, + "breaking_changes": [ + { + "type": "endpoint_removed", + "endpoint": "DELETE /users/{id}/avatar", + "severity": "high", + "migration": "Use PUT /users/{id} with avatar=null instead" + }, + { + "type": "required_field_added", + "location": "POST /users request body", + "field": "email_verified", + "severity": "high", + "migration": "Clients must now provide email_verified field" + }, + { + "type": "property_removed", + "schema": "User", + "property": "phoneNumber", + "severity": "medium", + "migration": "Use new phone_contacts array instead" + }, + { + "type": "type_changed", + "schema": "User", + "property": "age", + "old_type": "integer", + "new_type": "string", + "severity": "high", + "migration": "Convert age to string format" + } + ], + "non_breaking_changes": [ + { + "type": "endpoint_added", + "endpoint": "GET /users/{id}/preferences" + }, + { + "type": "optional_field_added", + "schema": "User", + "property": "preferences" + }, + { + "type": "response_field_added", + "endpoint": "GET /users/{id}", + "field": "last_login" + } + ], + "change_summary": { + "breaking": 4, + "additions": 2, + "modifications": 2, + "removals": 2 + } + } + ``` + +6. **Provide migration recommendations**: + ```markdown + ## Migration Guide: v1.0.0 → v2.0.0 + + ### Breaking Changes + + 1. **Removed endpoint: DELETE /users/{id}/avatar** + - **Impact**: High - Clients using this endpoint will fail + - **Migration**: Use PUT /users/{id} with avatar=null instead + - **Effort**: Low + + 2. **New required field: email_verified in POST /users** + - **Impact**: High - All user creation requests must include this field + - **Migration**: Update client code to provide email_verified boolean + - **Effort**: Medium + + 3. **Property removed: User.phoneNumber** + - **Impact**: Medium - Clients reading this field will get undefined + - **Migration**: Use User.phone_contacts array instead + - **Effort**: Medium + + 4. **Type changed: User.age (integer → string)** + - **Impact**: High - Type mismatch will cause deserialization errors + - **Migration**: Update models to use string type and convert existing data + - **Effort**: High + + ### Recommended Approach + 1. Implement migration layer for 2 versions + 2. Communicate breaking changes to consumers 30 days in advance + 3. Provide backward-compatible endpoints during transition period + 4. Monitor usage of deprecated endpoints + ``` + +## Error Handling + +| Scenario | Timeout | Behavior | +|----------|---------|----------| +| Spec load failure | N/A | Return error with file path details | +| Comparison failure | N/A | Return partial analysis with error context | +| Timeout | 120 seconds | Fails after 2 minutes | + +### On Success + +```json +{ + "status": "success", + "outputs": { + "compatibility_report": { + "compatible": false, + "breaking_changes": [...], + "non_breaking_changes": [...], + "change_summary": {...} + }, + "migration_recommendations": "...", + "api_diff_visualization": "..." + } +} +``` + +### On Failure + +```json +{ + "status": "failed", + "error_details": { + "error": "Failed to load old spec", + "file_path": "specs/user-service-v1.0.0.yaml", + "details": "File not found" + }, + "partial_analysis": null, + "suggested_fixes": [ + "Verify file path exists", + "Check file permissions" + ] +} +``` + +## Use Cases + +### 1. Pre-Release Validation + +Run before releasing a new API version to ensure backward compatibility: + +```yaml +# workflows/validate_release.yaml +steps: + - agent: api.analyzer + input: + old_spec_path: "specs/production/api-v1.yaml" + new_spec_path: "specs/staging/api-v2.yaml" + fail_on_breaking: true +``` + +### 2. Continuous Integration + +Integrate into CI/CD to prevent accidental breaking changes: + +```yaml +# .github/workflows/api-check.yml +- name: Check API Compatibility + run: | + # Agent runs via workflow.compose + python skills/workflow.compose/workflow_compose.py \ + workflows/check_api_compatibility.yaml +``` + +### 3. Documentation Generation + +Generate migration guides automatically: + +```bash +# Use agent output to create migration documentation +/api-compatibility old.yaml new.yaml > migration-guide.md +``` + +## Status + +**Draft** – This agent is under development and not yet marked active in the registry. + +## Related Documentation + +- [Agents Overview](../../docs/betty-architecture.md#layer-2-agents-reasoning-layer) – Understanding agents in Betty's architecture +- [Agent Schema Reference](../../docs/agent-schema-reference.md) – Agent manifest fields and structure +- [api.compatibility SKILL.md](../../skills/api.compatibility/SKILL.md) – Underlying compatibility check skill +- [API-Driven Development](../../docs/api-driven-development.md) – Full API workflow including compatibility checks + +## Version History + +- **0.1.0** (Oct 2025) – Initial draft implementation with oneshot analysis pattern diff --git a/agents/api.analyzer/agent.yaml b/agents/api.analyzer/agent.yaml new file mode 100644 index 0000000..98f985d --- /dev/null +++ b/agents/api.analyzer/agent.yaml @@ -0,0 +1,65 @@ +name: api.analyzer +version: 0.1.0 +description: "Analyze API specifications for backward compatibility and breaking changes" + +capabilities: + - Detect breaking changes between API versions + - Generate detailed compatibility reports + - Identify removed or modified endpoints + - Suggest migration paths for breaking changes + - Validate API evolution best practices + +skills_available: + - api.compatibility + - api.validate + +reasoning_mode: oneshot + +context_requirements: + old_spec_path: string + new_spec_path: string + fail_on_breaking: boolean + +workflow_pattern: | + 1. Load old and new API specifications + 2. Run comprehensive compatibility analysis + 3. Categorize changes as breaking or non-breaking + 4. Generate detailed report with migration recommendations + 5. Return results (no retry needed) + +example_task: | + Input: "Compare user-service v1.0.0 with v2.0.0 for breaking changes" + + Agent will: + 1. Load both specifications + 2. Analyze endpoint changes (additions, removals, modifications) + 3. Check for breaking schema changes + 4. Identify parameter or response format changes + 5. Generate compatibility report + 6. Provide migration recommendations + +error_handling: + timeout_seconds: 120 + on_spec_load_failure: "Return error with file path details" + on_comparison_failure: "Return partial analysis with error context" + +output: + success: + - Compatibility report (JSON) + - Breaking changes list + - Non-breaking changes list + - Migration recommendations + - API diff visualization + failure: + - Error details + - Partial analysis (if available) + - Suggested fixes + +status: draft + +tags: + - api + - analysis + - compatibility + - versioning + - oneshot diff --git a/agents/api.architect/README.md b/agents/api.architect/README.md new file mode 100644 index 0000000..ea8e2b6 --- /dev/null +++ b/agents/api.architect/README.md @@ -0,0 +1,50 @@ +# Api.Architect Agent + +## Purpose + +An agent that designs comprehensive REST APIs and validates them against best practices. Takes API requirements as input and produces validated OpenAPI specifications with generated data models ready for implementation. + +## Skills + +This agent uses the following skills: + +- `workflow.validate` +- `api.validate` +- `api.define` + +## Artifact Flow + +### Consumes + +- `API requirements` +- `Domain constraints and business rules` + +### Produces + +- `openapi-spec` +- `api-models` +- `validation-report` + +## Example Use Cases + +- Design a RESTful API for an e-commerce platform with products, orders, and customers +- Create an API for a task management system with projects, tasks, and user assignments +- Design a multi-tenant SaaS API with proper authentication and authorization + +## Usage + +```bash +# Activate the agent +/agent api.architect + +# Or invoke directly +betty agent run api.architect --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/api.architect/agent.yaml b/agents/api.architect/agent.yaml new file mode 100644 index 0000000..4c92af8 --- /dev/null +++ b/agents/api.architect/agent.yaml @@ -0,0 +1,36 @@ +name: api.architect +version: 0.1.0 +description: An agent that designs comprehensive REST APIs and validates them against + best practices. Takes API requirements as input and produces validated OpenAPI specifications + with generated data models ready for implementation. +status: draft +reasoning_mode: iterative +capabilities: + - Translate API requirements into detailed OpenAPI specifications + - Validate API designs against organizational standards and linting rules + - Generate reference data models to accelerate implementation +skills_available: + - workflow.validate + - api.validate + - api.define +permissions: [] +artifact_metadata: + consumes: + - type: API requirements + description: Input artifact of type API requirements + - type: Domain constraints and business rules + description: Input artifact of type Domain constraints and business rules + produces: + - type: openapi-spec + schema: schemas/openapi-spec.json + file_pattern: '*.openapi.yaml' + content_type: application/yaml + description: OpenAPI 3.0+ specification + - type: api-models + file_pattern: '*.{py,ts,go}' + description: Generated API data models + - type: validation-report + schema: schemas/validation-report.json + file_pattern: '*.validation.json' + content_type: application/json + description: Structured validation results diff --git a/agents/api.designer/README.md b/agents/api.designer/README.md new file mode 100644 index 0000000..bcde318 --- /dev/null +++ b/agents/api.designer/README.md @@ -0,0 +1,217 @@ +# api.designer Agent + +## Purpose + +**api.designer** is an intelligent agent that orchestrates the API design process from natural language requirements to validated, production-ready OpenAPI specifications with generated models. + +This agent uses iterative refinement to create APIs that comply with enterprise guidelines (Zalando by default), automatically fixing validation errors and ensuring best practices. + +## Behavior + +- **Reasoning Mode**: `iterative` – The agent retries on validation failures, refining the spec until it passes all checks +- **Capabilities**: + - Design RESTful APIs from natural language requirements + - Apply Zalando guidelines automatically (or other guideline sets) + - Generate OpenAPI 3.1 specs with best practices + - Iteratively refine based on validation feedback + - Handle AsyncAPI for event-driven architectures + +## Skills Used + +The agent has access to the following skills and uses them in sequence: + +| Skill | Purpose | +|-------|---------| +| `api.define` | Scaffolds initial OpenAPI spec from service name and requirements | +| `api.validate` | Validates spec against enterprise guidelines (Zalando, Google, Microsoft) | +| `api.generate-models` | Generates type-safe models in target languages (TypeScript, Python, etc.) | +| `api.compatibility` | Checks for breaking changes when updating existing APIs | + +## Workflow Pattern + +The agent follows this iterative pattern: + +``` +1. Analyze requirements and domain context +2. Draft OpenAPI spec following guidelines +3. Run validation (api.validate) +4. If validation fails: + - Analyze errors + - Refine spec + - Re-validate + - Repeat until passing (max 3 retries) +5. Generate models for target languages +6. Verify generated models compile +``` + +## Manifest Fields (Quick Reference) + +```yaml +name: api.designer +version: 0.1.0 +reasoning_mode: iterative +skills_available: + - api.define + - api.validate + - api.generate-models + - api.compatibility +status: draft +``` + +## Usage + +This agent is invoked through a command or workflow: + +### Via Slash Command + +```bash +# Assuming /api-design command is registered to use this agent +/api-design user-service +``` + +The command passes the service name to the agent, which then: +1. Uses `api.define` to create initial spec +2. Validates with `api.validate` +3. Fixes any validation errors iteratively +4. Generates models with `api.generate-models` + +### Via Workflow + +Include the agent in a workflow YAML: + +```yaml +# workflows/design_api.yaml +steps: + - agent: api.designer + input: + service_name: "user-service" + guidelines: "zalando" + languages: ["typescript", "python"] +``` + +## Context Requirements + +The agent expects the following context: + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `guidelines` | string | Which API guidelines to follow | `"zalando"`, `"google"`, `"microsoft"` | +| `domain` | string | Business domain for API design | `"user-management"`, `"e-commerce"` | +| `existing_apis` | list | Related APIs to maintain consistency with | `["auth-service", "notification-service"]` | +| `strict_mode` | boolean | Whether to treat warnings as errors | `true` or `false` | + +## Example Task + +**Input**: +``` +"Create API for user management with CRUD operations, + authentication via JWT, and email verification workflow" +``` + +**Agent execution**: + +1. **Draft OpenAPI spec** with proper resource paths: + - `POST /users` - Create user + - `GET /users/{id}` - Get user + - `PUT /users/{id}` - Update user + - `DELETE /users/{id}` - Delete user + - `POST /users/{id}/verify-email` - Email verification + +2. **Apply Zalando guidelines**: + - Use snake_case for property names + - Include problem JSON error responses + - Add required headers (X-Request-ID, etc.) + - Define proper HTTP status codes + +3. **Validate spec** using `api.validate`: + ```bash + python skills/api.validate/api_validate.py specs/user-service.yaml zalando + ``` + +4. **Fix validation issues** (if any): + - Missing required headers → Add to spec + - Incorrect naming → Convert to snake_case + - Missing error schemas → Add problem JSON schemas + +5. **Generate models** using Modelina: + ```bash + python skills/api.generate-models/modelina_generate.py \ + specs/user-service.yaml typescript src/models/typescript + python skills/api.generate-models/modelina_generate.py \ + specs/user-service.yaml python src/models/python + ``` + +6. **Verify models compile**: + - TypeScript: `tsc --noEmit` + - Python: `mypy --strict` + +## Error Handling + +| Scenario | Max Retries | Behavior | +|----------|-------------|----------| +| Validation failure | 3 | Analyze errors, refine spec, retry | +| Model generation failure | 3 | Try alternative Modelina configurations | +| Compilation failure | 3 | Adjust spec to fix type issues | +| Timeout | N/A | Fails after 300 seconds (5 minutes) | + +### On Success + +```json +{ + "status": "success", + "outputs": { + "spec_path": "specs/user-service.openapi.yaml", + "validation_report": { + "valid": true, + "errors": [], + "warnings": [] + }, + "generated_models": { + "typescript": ["src/models/typescript/User.ts", "src/models/typescript/UserResponse.ts"], + "python": ["src/models/python/user.py", "src/models/python/user_response.py"] + }, + "dependency_graph": "..." + } +} +``` + +### On Failure + +```json +{ + "status": "failed", + "error_analysis": { + "step": "validation", + "attempts": 3, + "last_error": "Missing required header: X-Request-ID in all endpoints" + }, + "partial_spec": "specs/user-service.openapi.yaml", + "suggested_fixes": [ + "Add X-Request-ID header to all operations", + "See Zalando guidelines: https://..." + ] +} +``` + +## Status + +**Draft** – This agent is under development and not yet marked active in the registry. Current goals for next version: + +- [ ] Improve prompt engineering for better initial API designs +- [ ] Add more robust error handling for iterative loops +- [ ] Support for more guideline sets (Google, Microsoft) +- [ ] Better context injection from existing APIs +- [ ] Automatic testing of generated models + +## Related Documentation + +- [Agents Overview](../../docs/betty-architecture.md#layer-2-agents-reasoning-layer) – Understanding agents in Betty's architecture +- [Agent Schema Reference](../../docs/agent-schema-reference.md) – Agent manifest fields and structure +- [API-Driven Development](../../docs/api-driven-development.md) – Full API design workflow using Betty +- [api.define SKILL.md](../../skills/api.define/SKILL.md) – Skill for creating API specs +- [api.validate SKILL.md](../../skills/api.validate/SKILL.md) – Skill for validating specs +- [api.generate-models SKILL.md](../../skills/api.generate-models/SKILL.md) – Skill for generating models + +## Version History + +- **0.1.0** (Oct 2025) – Initial draft implementation with iterative refinement pattern diff --git a/agents/api.designer/agent.yaml b/agents/api.designer/agent.yaml new file mode 100644 index 0000000..f11b431 --- /dev/null +++ b/agents/api.designer/agent.yaml @@ -0,0 +1,78 @@ +name: api.designer +version: 0.1.0 +description: "Design RESTful APIs following enterprise guidelines with iterative refinement" + +capabilities: + - Design RESTful APIs from natural language requirements + - Apply Zalando guidelines automatically + - Generate OpenAPI 3.1 specs with best practices + - Iteratively refine based on validation feedback + - Handle AsyncAPI for event-driven architectures + +skills_available: + - api.define + - api.validate + - api.generatemodels + - api.compatibility + +reasoning_mode: iterative + +context_requirements: + guidelines: string + domain: string + existing_apis: list + strict_mode: boolean + +workflow_pattern: | + 1. Analyze requirements and domain context + 2. Draft OpenAPI spec following guidelines + 3. Run validation (api.validate) + 4. If validation fails: + - Analyze errors + - Refine spec + - Re-validate + - Repeat until passing + 5. Generate models for target languages + 6. Verify generated models compile + +example_task: | + Input: "Create API for user management with CRUD operations, + authentication via JWT, and email verification workflow" + + Agent will: + 1. Draft OpenAPI spec with proper resource paths (/users, /users/{id}) + 2. Apply Zalando guidelines (snake_case, problem JSON, etc.) + 3. Validate spec against Zally rules + 4. Fix issues (e.g., add required headers, fix naming) + 5. Generate TypeScript and Python models via Modelina + 6. Verify models compile in sample projects + +error_handling: + max_retries: 3 + on_validation_failure: "Analyze errors, refine spec, retry" + on_generation_failure: "Try alternative Modelina configurations" + on_compilation_failure: "Adjust spec to fix type issues" + timeout_seconds: 300 + +output: + success: + - OpenAPI spec (validated) + - Generated models (compiled) + - Validation report + - Dependency graph + failure: + - Error analysis + - Partial spec + - Suggested fixes + +status: draft + +tags: + - api + - design + - openapi + - zalando + - iterative + +dependencies: + - context.schema diff --git a/agents/api.orchestrator/README.md b/agents/api.orchestrator/README.md new file mode 100644 index 0000000..90f7140 --- /dev/null +++ b/agents/api.orchestrator/README.md @@ -0,0 +1,44 @@ +# Api.Orchestrator Agent + +Orchestrates complete API lifecycle from design through testing and deployment + +## Purpose + +This orchestrator agent coordinates complex api workflows by composing and sequencing multiple skills. It handles the complete lifecycle from planning through execution and validation. + +## Capabilities + +- Coordinate API design, validation, and compatibility checking +- Manage API generation and model creation workflows +- Orchestrate testing and quality assurance +- Handle API versioning and documentation +- Coordinate deployment and publishing + +## Available Skills + +- `api.define` +- `api.validate` +- `api.compatibility` +- `api.generatemodels` +- `api.test` + +## Usage + +This agent uses iterative reasoning to: +1. Analyze requirements +2. Plan execution steps +3. Coordinate skill execution +4. Validate results +5. Handle errors and retries + +## Status + +**Generated**: Auto-generated from taxonomy gap analysis + +## Next Steps + +- [ ] Review and refine capabilities +- [ ] Test with real workflows +- [ ] Add domain-specific examples +- [ ] Integrate with existing agents +- [ ] Document best practices diff --git a/agents/api.orchestrator/agent.yaml b/agents/api.orchestrator/agent.yaml new file mode 100644 index 0000000..2d53075 --- /dev/null +++ b/agents/api.orchestrator/agent.yaml @@ -0,0 +1,53 @@ +name: api.orchestrator +version: 0.1.0 +description: Orchestrates complete API lifecycle from design through testing and deployment +capabilities: +- Coordinate API design, validation, and compatibility checking +- Manage API generation and model creation workflows +- Orchestrate testing and quality assurance +- Handle API versioning and documentation +- Coordinate deployment and publishing +skills_available: +- api.define +- api.validate +- api.compatibility +- api.generatemodels +- api.test +reasoning_mode: iterative +tags: +- api +- orchestration +- workflow +- lifecycle +workflow_pattern: '1. Analyze incoming request and requirements + + 2. Identify relevant api skills and workflows + + 3. Compose multi-step execution plan + + 4. Execute skills in coordinated sequence + + 5. Validate intermediate results + + 6. Handle errors and retry as needed + + 7. Return comprehensive results' +example_task: "Input: \"Complete api workflow from start to finish\"\n\nAgent will:\n\ + 1. Break down the task into stages\n2. Select appropriate skills for each stage\n\ + 3. Execute create \u2192 validate \u2192 review \u2192 publish lifecycle\n4. Monitor\ + \ progress and handle failures\n5. Generate comprehensive reports" +error_handling: + timeout_seconds: 300 + retry_strategy: exponential_backoff + max_retries: 3 +output: + success: + - Api workflow results + - Execution logs and metrics + - Validation reports + - Generated artifacts + failure: + - Error details and stack traces + - Partial results (if available) + - Remediation suggestions +status: generated diff --git a/agents/code.reviewer/README.md b/agents/code.reviewer/README.md new file mode 100644 index 0000000..cf64e18 --- /dev/null +++ b/agents/code.reviewer/README.md @@ -0,0 +1,48 @@ +# Code.Reviewer Agent + +## Purpose + +Analyzes code changes and provides comprehensive feedback on code quality, security vulnerabilities, performance issues, and adherence to best practices. + +## Skills + +This agent uses the following skills: + + +## Artifact Flow + +### Consumes + +- `code-diff` +- `coding-standards` + +### Produces + +- `review-report` +- `suggestion-list` +- `static-analysis` +- `security-scan` +- `style-check` +- `List of issues found with line numbers` +- `Severity and category for each issue` +- `Suggested fixes with code examples` +- `Overall code quality score` +- `Compliance status with coding standards` + +## Usage + +```bash +# Activate the agent +/agent code.reviewer + +# Or invoke directly +betty agent run code.reviewer --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/code.reviewer/agent.yaml b/agents/code.reviewer/agent.yaml new file mode 100644 index 0000000..a632fbe --- /dev/null +++ b/agents/code.reviewer/agent.yaml @@ -0,0 +1,42 @@ +name: code.reviewer +version: 0.1.0 +description: Analyzes code changes and provides comprehensive feedback on code quality, + security vulnerabilities, performance issues, and adherence to best practices. +status: draft +reasoning_mode: iterative +capabilities: + - Review diffs for quality, security, and maintainability concerns + - Generate prioritized issue lists with remediation guidance + - Summarize overall code health and compliance with standards +skills_available: + - code.format + - test.workflow.integration + - policy.enforce +permissions: [] +artifact_metadata: + consumes: + - type: code-diff + description: Input artifact of type code-diff + - type: coding-standards + description: Input artifact of type coding-standards + produces: + - type: review-report + description: Output artifact of type review-report + - type: suggestion-list + description: Output artifact of type suggestion-list + - type: static-analysis + description: Output artifact of type static-analysis + - type: security-scan + description: Output artifact of type security-scan + - type: style-check + description: Output artifact of type style-check + - type: List of issues found with line numbers + description: Output artifact of type List of issues found with line numbers + - type: Severity and category for each issue + description: Output artifact of type Severity and category for each issue + - type: Suggested fixes with code examples + description: Output artifact of type Suggested fixes with code examples + - type: Overall code quality score + description: Output artifact of type Overall code quality score + - type: Compliance status with coding standards + description: Output artifact of type Compliance status with coding standards diff --git a/agents/data.architect/README.md b/agents/data.architect/README.md new file mode 100644 index 0000000..29455d4 --- /dev/null +++ b/agents/data.architect/README.md @@ -0,0 +1,70 @@ +# Data.Architect Agent + +## Purpose + +Create comprehensive data architecture and governance artifacts including data models, schema definitions, data flow diagrams, data dictionaries, data governance policies, and data quality frameworks. Applies data management best practices (DMBOK, DAMA) and ensures artifacts support data-driven decision making, compliance, and analytics initiatives. + +## Skills + +This agent uses the following skills: + + +## Artifact Flow + +### Consumes + +- `Business requirements or use cases` +- `Data sources and systems` +- `Data domains or subject areas` +- `Compliance requirements` +- `Data quality expectations` +- `Analytics or reporting needs` + +### Produces + +- `data-model: Logical and physical data models with entities, relationships, and attributes` +- `schema-definition: Database schemas with tables, columns, constraints, and indexes` +- `data-flow-diagram: Data flow between systems with transformations and quality checks` +- `data-dictionary: Comprehensive data dictionary with business definitions` +- `data-governance-policy: Data governance framework with roles, policies, and procedures` +- `data-quality-framework: Data quality measurement and monitoring framework` +- `master-data-management-plan: MDM strategy for critical data domains` +- `data-lineage-diagram: End-to-end data lineage with source-to-target mappings` +- `data-catalog: Enterprise data catalog with metadata and discovery` + +## Example Use Cases + +- Entities: Customer, Account, Contact, Interaction, Order, SupportTicket, Product +- Relationships and cardinality +- Attributes with data types and constraints +- Integration patterns for source systems +- Master data management approach +- Data quality rules +- Data governance organization and roles (CDO, data stewards, owners) +- Data classification and handling policies +- Data quality standards and SLAs +- Metadata management standards +- GDPR compliance procedures (consent, right to erasure) +- SOX data retention and audit requirements +- Data access control policies +- data-flow-diagram.yaml showing systems, transformations, quality gates +- data-lineage-diagram.yaml with source-to-target mappings +- data-quality-framework.yaml with validation rules and monitoring + +## Usage + +```bash +# Activate the agent +/agent data.architect + +# Or invoke directly +betty agent run data.architect --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/data.architect/agent.yaml b/agents/data.architect/agent.yaml new file mode 100644 index 0000000..501a9e8 --- /dev/null +++ b/agents/data.architect/agent.yaml @@ -0,0 +1,66 @@ +name: data.architect +version: 0.1.0 +description: Create comprehensive data architecture and governance artifacts including + data models, schema definitions, data flow diagrams, data dictionaries, data governance + policies, and data quality frameworks. Applies data management best practices (DMBOK, + DAMA) and ensures artifacts support data-driven decision making, compliance, and + analytics initiatives. +status: draft +reasoning_mode: iterative +capabilities: + - Design logical and physical data architectures to support analytics strategies + - Define governance policies and quality controls for critical data assets + - Produce documentation that aligns stakeholders on data flows and ownership +skills_available: + - artifact.create + - artifact.validate + - artifact.review +permissions: + - filesystem:read + - filesystem:write +artifact_metadata: + consumes: + - type: Business requirements or use cases + description: Input artifact of type Business requirements or use cases + - type: Data sources and systems + description: Input artifact of type Data sources and systems + - type: Data domains or subject areas + description: Input artifact of type Data domains or subject areas + - type: Compliance requirements + description: Input artifact of type Compliance requirements + - type: Data quality expectations + description: Input artifact of type Data quality expectations + - type: Analytics or reporting needs + description: Input artifact of type Analytics or reporting needs + produces: + - type: 'data-model: Logical and physical data models with entities, relationships, + and attributes' + description: 'Output artifact of type data-model: Logical and physical data models + with entities, relationships, and attributes' + - type: 'schema-definition: Database schemas with tables, columns, constraints, + and indexes' + description: 'Output artifact of type schema-definition: Database schemas with + tables, columns, constraints, and indexes' + - type: 'data-flow-diagram: Data flow between systems with transformations and quality + checks' + description: 'Output artifact of type data-flow-diagram: Data flow between systems + with transformations and quality checks' + - type: 'data-dictionary: Comprehensive data dictionary with business definitions' + description: 'Output artifact of type data-dictionary: Comprehensive data dictionary + with business definitions' + - type: 'data-governance-policy: Data governance framework with roles, policies, + and procedures' + description: 'Output artifact of type data-governance-policy: Data governance + framework with roles, policies, and procedures' + - type: 'data-quality-framework: Data quality measurement and monitoring framework' + description: 'Output artifact of type data-quality-framework: Data quality measurement + and monitoring framework' + - type: 'master-data-management-plan: MDM strategy for critical data domains' + description: 'Output artifact of type master-data-management-plan: MDM strategy + for critical data domains' + - type: 'data-lineage-diagram: End-to-end data lineage with source-to-target mappings' + description: 'Output artifact of type data-lineage-diagram: End-to-end data lineage + with source-to-target mappings' + - type: 'data-catalog: Enterprise data catalog with metadata and discovery' + description: 'Output artifact of type data-catalog: Enterprise data catalog with + metadata and discovery' diff --git a/agents/data.orchestrator/README.md b/agents/data.orchestrator/README.md new file mode 100644 index 0000000..0454968 --- /dev/null +++ b/agents/data.orchestrator/README.md @@ -0,0 +1,42 @@ +# Data.Orchestrator Agent + +Orchestrates data workflows including transformation, validation, and quality assurance + +## Purpose + +This orchestrator agent coordinates complex data workflows by composing and sequencing multiple skills. It handles the complete lifecycle from planning through execution and validation. + +## Capabilities + +- Coordinate data transformation pipelines +- Manage data validation and quality checks +- Orchestrate data migration workflows +- Handle data governance and compliance +- Coordinate analytics and reporting + +## Available Skills + +- `data.transform` +- `workflow.validate` +- `workflow.compose` + +## Usage + +This agent uses iterative reasoning to: +1. Analyze requirements +2. Plan execution steps +3. Coordinate skill execution +4. Validate results +5. Handle errors and retries + +## Status + +**Generated**: Auto-generated from taxonomy gap analysis + +## Next Steps + +- [ ] Review and refine capabilities +- [ ] Test with real workflows +- [ ] Add domain-specific examples +- [ ] Integrate with existing agents +- [ ] Document best practices diff --git a/agents/data.orchestrator/agent.yaml b/agents/data.orchestrator/agent.yaml new file mode 100644 index 0000000..5878422 --- /dev/null +++ b/agents/data.orchestrator/agent.yaml @@ -0,0 +1,52 @@ +name: data.orchestrator +version: 0.1.0 +description: Orchestrates data workflows including transformation, validation, and + quality assurance +capabilities: +- Coordinate data transformation pipelines +- Manage data validation and quality checks +- Orchestrate data migration workflows +- Handle data governance and compliance +- Coordinate analytics and reporting +skills_available: +- data.transform +- workflow.validate +- workflow.compose +reasoning_mode: iterative +tags: +- data +- orchestration +- workflow +- etl +workflow_pattern: '1. Analyze incoming request and requirements + + 2. Identify relevant data skills and workflows + + 3. Compose multi-step execution plan + + 4. Execute skills in coordinated sequence + + 5. Validate intermediate results + + 6. Handle errors and retry as needed + + 7. Return comprehensive results' +example_task: "Input: \"Complete data workflow from start to finish\"\n\nAgent will:\n\ + 1. Break down the task into stages\n2. Select appropriate skills for each stage\n\ + 3. Execute create \u2192 validate \u2192 review \u2192 publish lifecycle\n4. Monitor\ + \ progress and handle failures\n5. Generate comprehensive reports" +error_handling: + timeout_seconds: 300 + retry_strategy: exponential_backoff + max_retries: 3 +output: + success: + - Data workflow results + - Execution logs and metrics + - Validation reports + - Generated artifacts + failure: + - Error details and stack traces + - Partial results (if available) + - Remediation suggestions +status: generated diff --git a/agents/data.validator/README.md b/agents/data.validator/README.md new file mode 100644 index 0000000..4d9eb89 --- /dev/null +++ b/agents/data.validator/README.md @@ -0,0 +1,55 @@ +# Data.Validator Agent + +## Purpose + +Validates data files against schemas, business rules, and data quality standards. Ensures data integrity, completeness, and compliance. + +## Skills + +This agent uses the following skills: + +- `workflow.validate` +- `api.validate` + +## Artifact Flow + +### Consumes + +- `data-file` +- `schema-definition` +- `validation-rules` + +### Produces + +- `validation-report` +- `data-quality-metrics` +- `data.validatejson` +- `schema.validate` +- `data.profile` +- `Structural: Schema and format validation` +- `Semantic: Business rule validation` +- `Statistical: Data quality profiling` +- `Validation status` +- `List of violations with severity` +- `Data quality score` +- `Statistics` +- `Recommendations for fixing issues` +- `Compliance status with standards` + +## Usage + +```bash +# Activate the agent +/agent data.validator + +# Or invoke directly +betty agent run data.validator --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/data.validator/agent.yaml b/agents/data.validator/agent.yaml new file mode 100644 index 0000000..e005e2a --- /dev/null +++ b/agents/data.validator/agent.yaml @@ -0,0 +1,54 @@ +name: data.validator +version: 0.1.0 +description: Validates data files against schemas, business rules, and data quality + standards. Ensures data integrity, completeness, and compliance. +status: draft +reasoning_mode: iterative +capabilities: + - Validate datasets against structural and semantic rules + - Generate detailed issue reports with remediation recommendations + - Track quality metrics and highlight compliance gaps +skills_available: + - workflow.validate + - api.validate +permissions: [] +artifact_metadata: + consumes: + - type: data-file + description: Input artifact of type data-file + - type: schema-definition + description: Input artifact of type schema-definition + - type: validation-rules + description: Input artifact of type validation-rules + produces: + - type: validation-report + schema: schemas/validation-report.json + file_pattern: '*.validation.json' + content_type: application/json + description: Structured validation results + - type: data-quality-metrics + description: Output artifact of type data-quality-metrics + - type: data.validatejson + description: Output artifact of type data.validatejson + - type: schema.validate + description: Output artifact of type schema.validate + - type: data.profile + description: Output artifact of type data.profile + - type: 'Structural: Schema and format validation' + description: 'Output artifact of type Structural: Schema and format validation' + - type: 'Semantic: Business rule validation' + description: 'Output artifact of type Semantic: Business rule validation' + - type: 'Statistical: Data quality profiling' + description: 'Output artifact of type Statistical: Data quality profiling' + - type: Validation status + description: Output artifact of type Validation status + - type: List of violations with severity + description: Output artifact of type List of violations with severity + - type: Data quality score + description: Output artifact of type Data quality score + - type: Statistics + description: Output artifact of type Statistics + - type: Recommendations for fixing issues + description: Output artifact of type Recommendations for fixing issues + - type: Compliance status with standards + description: Output artifact of type Compliance status with standards diff --git a/agents/deployment.engineer/README.md b/agents/deployment.engineer/README.md new file mode 100644 index 0000000..ec4ecd1 --- /dev/null +++ b/agents/deployment.engineer/README.md @@ -0,0 +1,79 @@ +# Deployment.Engineer Agent + +## Purpose + +Create comprehensive deployment and release artifacts including deployment plans, CI/CD pipelines, release checklists, rollback procedures, runbooks, and infrastructure-as-code configurations. Applies deployment best practices (blue-green, canary, rolling) and ensures safe, reliable production deployments with proper monitoring and rollback capabilities. + +## Skills + +This agent uses the following skills: + + +## Artifact Flow + +### Consumes + +- `Application or service description` +- `Infrastructure and environment details` +- `Deployment requirements` +- `Release scope and components` +- `Monitoring and alerting requirements` +- `Compliance or change control requirements` + +### Produces + +- `deployment-plan: Comprehensive deployment strategy with steps, validation, and rollback` +- `cicd-pipeline-definition: CI/CD pipeline configuration with stages, gates, and automation` +- `release-checklist: Pre-deployment checklist with validation and approval steps` +- `rollback-plan: Rollback procedures with triggers and recovery steps` +- `runbooks: Operational runbooks for deployment, troubleshooting, and maintenance` +- `infrastructure-as-code: Infrastructure provisioning templates` +- `deployment-pipeline: Deployment automation scripts and orchestration` +- `smoke-test-suite: Post-deployment smoke tests for validation` +- `production-readiness-checklist: Production readiness assessment and sign-off` + +## Example Use Cases + +- Deployment strategy (blue-green with traffic shifting) +- Pre-deployment checklist (backups, capacity validation) +- Deployment sequence and dependencies +- Health checks and validation gates +- Traffic migration steps (0% → 10% → 50% → 100%) +- Rollback triggers and procedures +- Post-deployment validation +- Monitoring and alerting configuration +- Communication plan +- Build stage (npm install, compile, bundle) +- Test stage (unit tests, integration tests, coverage gate 80%) +- Security stage (SAST, dependency scanning, OWASP check) +- Deploy to staging (automated) +- Smoke tests and integration tests in staging +- Manual approval gate for production +- Deploy to production (blue-green) +- Post-deployment validation +- Slack/email notifications +- Deployment runbook (step-by-step deployment procedures) +- Scaling runbook (horizontal and vertical scaling procedures) +- Troubleshooting runbook (common issues and resolution) +- Incident response runbook (incident classification and escalation) +- Disaster recovery runbook (backup and restore procedures) +- Database maintenance runbook (schema changes, backups) +- Each runbook includes: prerequisites, steps, validation, rollback + +## Usage + +```bash +# Activate the agent +/agent deployment.engineer + +# Or invoke directly +betty agent run deployment.engineer --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/deployment.engineer/agent.yaml b/agents/deployment.engineer/agent.yaml new file mode 100644 index 0000000..e9c6d06 --- /dev/null +++ b/agents/deployment.engineer/agent.yaml @@ -0,0 +1,65 @@ +name: deployment.engineer +version: 0.1.0 +description: Create comprehensive deployment and release artifacts including deployment + plans, CI/CD pipelines, release checklists, rollback procedures, runbooks, and infrastructure-as-code + configurations. Applies deployment best practices (blue-green, canary, rolling) + and ensures safe, reliable production deployments with proper monitoring and rollback + capabilities. +status: draft +reasoning_mode: iterative +capabilities: + - Design deployment strategies with rollback and validation procedures + - Automate delivery pipelines and operational runbooks + - Coordinate release governance, approvals, and compliance requirements +skills_available: + - artifact.create + - artifact.validate + - artifact.review +permissions: + - filesystem:read + - filesystem:write +artifact_metadata: + consumes: + - type: Application or service description + description: Input artifact of type Application or service description + - type: Infrastructure and environment details + description: Input artifact of type Infrastructure and environment details + - type: Deployment requirements + description: Input artifact of type Deployment requirements + - type: Release scope and components + description: Input artifact of type Release scope and components + - type: Monitoring and alerting requirements + description: Input artifact of type Monitoring and alerting requirements + - type: Compliance or change control requirements + description: Input artifact of type Compliance or change control requirements + produces: + - type: 'deployment-plan: Comprehensive deployment strategy with steps, validation, + and rollback' + description: 'Output artifact of type deployment-plan: Comprehensive deployment + strategy with steps, validation, and rollback' + - type: 'cicd-pipeline-definition: CI/CD pipeline configuration with stages, gates, + and automation' + description: 'Output artifact of type cicd-pipeline-definition: CI/CD pipeline + configuration with stages, gates, and automation' + - type: 'release-checklist: Pre-deployment checklist with validation and approval + steps' + description: 'Output artifact of type release-checklist: Pre-deployment checklist + with validation and approval steps' + - type: 'rollback-plan: Rollback procedures with triggers and recovery steps' + description: 'Output artifact of type rollback-plan: Rollback procedures with + triggers and recovery steps' + - type: 'runbooks: Operational runbooks for deployment, troubleshooting, and maintenance' + description: 'Output artifact of type runbooks: Operational runbooks for deployment, + troubleshooting, and maintenance' + - type: 'infrastructure-as-code: Infrastructure provisioning templates' + description: 'Output artifact of type infrastructure-as-code: Infrastructure provisioning + templates' + - type: 'deployment-pipeline: Deployment automation scripts and orchestration' + description: 'Output artifact of type deployment-pipeline: Deployment automation + scripts and orchestration' + - type: 'smoke-test-suite: Post-deployment smoke tests for validation' + description: 'Output artifact of type smoke-test-suite: Post-deployment smoke + tests for validation' + - type: 'production-readiness-checklist: Production readiness assessment and sign-off' + description: 'Output artifact of type production-readiness-checklist: Production + readiness assessment and sign-off' diff --git a/agents/file.processor/README.md b/agents/file.processor/README.md new file mode 100644 index 0000000..3a0abf7 --- /dev/null +++ b/agents/file.processor/README.md @@ -0,0 +1,52 @@ +# File.Processor Agent + +## Purpose + +Processes files through various transformations including format conversion, compression, encryption, and batch operations. + +## Skills + +This agent uses the following skills: + + +## Artifact Flow + +### Consumes + +- `file-list` +- `transformation-config` + +### Produces + +- `processed-files` +- `processing-report` +- `file.convert` +- `file.compress` +- `file.encrypt` +- `batch.processor` +- `Sequential: Process files one by one` +- `Parallel: Process multiple files concurrently` +- `Pipeline: Chain multiple transformations` +- `Files processed successfully` +- `Files that failed with error details` +- `Processing time and performance metrics` +- `Storage space saved` +- `Transformation details for each file` + +## Usage + +```bash +# Activate the agent +/agent file.processor + +# Or invoke directly +betty agent run file.processor --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/file.processor/agent.yaml b/agents/file.processor/agent.yaml new file mode 100644 index 0000000..30ef095 --- /dev/null +++ b/agents/file.processor/agent.yaml @@ -0,0 +1,50 @@ +name: file.processor +version: 0.1.0 +description: Processes files through various transformations including format conversion, + compression, encryption, and batch operations. +status: draft +reasoning_mode: oneshot +capabilities: + - Execute configurable pipelines of file transformations + - Optimize files through compression and format conversion workflows + - Apply encryption and verification steps with detailed reporting +skills_available: + - file.compare + - workflow.orchestrate + - build.optimize +permissions: [] +artifact_metadata: + consumes: + - type: file-list + description: Input artifact of type file-list + - type: transformation-config + description: Input artifact of type transformation-config + produces: + - type: processed-files + description: Output artifact of type processed-files + - type: processing-report + description: Output artifact of type processing-report + - type: file.convert + description: Output artifact of type file.convert + - type: file.compress + description: Output artifact of type file.compress + - type: file.encrypt + description: Output artifact of type file.encrypt + - type: batch.processor + description: Output artifact of type batch.processor + - type: 'Sequential: Process files one by one' + description: 'Output artifact of type Sequential: Process files one by one' + - type: 'Parallel: Process multiple files concurrently' + description: 'Output artifact of type Parallel: Process multiple files concurrently' + - type: 'Pipeline: Chain multiple transformations' + description: 'Output artifact of type Pipeline: Chain multiple transformations' + - type: Files processed successfully + description: Output artifact of type Files processed successfully + - type: Files that failed with error details + description: Output artifact of type Files that failed with error details + - type: Processing time and performance metrics + description: Output artifact of type Processing time and performance metrics + - type: Storage space saved + description: Output artifact of type Storage space saved + - type: Transformation details for each file + description: Output artifact of type Transformation details for each file diff --git a/agents/governance.manager/README.md b/agents/governance.manager/README.md new file mode 100644 index 0000000..629a1de --- /dev/null +++ b/agents/governance.manager/README.md @@ -0,0 +1,73 @@ +# Governance.Manager Agent + +## Purpose + +Create comprehensive program and project governance artifacts including project charters, RAID logs (Risks, Assumptions, Issues, Decisions), decision logs, governance frameworks, compliance matrices, and steering committee artifacts. Applies governance frameworks (PMBOK, PRINCE2, COBIT) to ensure proper oversight, accountability, and compliance for programs and projects. + +## Skills + +This agent uses the following skills: + + +## Artifact Flow + +### Consumes + +- `Program or project description` +- `Stakeholders and governance structure` +- `Objectives and success criteria` +- `Compliance or regulatory requirements` +- `Risks, issues, and assumptions` +- `Decisions to be documented` + +### Produces + +- `project-charter: Project charter with authority, scope, objectives, and success criteria` +- `raid-log: Comprehensive RAID log` +- `decision-log: Decision register with context, options, rationale, and outcomes` +- `governance-framework: Governance structure with roles, committees, and decision rights` +- `compliance-matrix: Compliance mapping to regulatory and policy requirements` +- `stakeholder-analysis: Stakeholder analysis with power/interest grid and engagement strategy` +- `steering-committee-report: Executive steering committee reporting pack` +- `change-control-process: Change management and approval workflow` +- `benefits-realization-plan: Benefits tracking and realization framework` + +## Example Use Cases + +- Project purpose and business justification +- Scope and deliverables (migrate 50 applications to AWS) +- Objectives and success criteria (90% cost reduction, zero downtime) +- Authority and decision rights +- Governance structure (steering committee, PMO oversight) +- Budget and resource allocation +- Assumptions and constraints +- Approval signatures +- Risks: 15-20 identified risks with impact, probability, mitigation +- Assumptions: Business continuity, vendor SLAs, budget availability +- Issues: Current issues with severity, owner, and resolution plan +- Decisions: Key decisions with rationale and stakeholder approval +- Cross-references to related artifacts +- Governance structure (executive steering, program board, workstream leads) +- Decision-making authority and escalation paths +- Meeting cadence and reporting requirements +- RACI matrix for key decisions and deliverables +- Compliance and risk management processes +- Change control and approval workflow + +## Usage + +```bash +# Activate the agent +/agent governance.manager + +# Or invoke directly +betty agent run governance.manager --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/governance.manager/agent.yaml b/agents/governance.manager/agent.yaml new file mode 100644 index 0000000..520f572 --- /dev/null +++ b/agents/governance.manager/agent.yaml @@ -0,0 +1,64 @@ +name: governance.manager +version: 0.1.0 +description: Create comprehensive program and project governance artifacts including + project charters, RAID logs (Risks, Assumptions, Issues, Decisions), decision logs, + governance frameworks, compliance matrices, and steering committee artifacts. Applies + governance frameworks (PMBOK, PRINCE2, COBIT) to ensure proper oversight, accountability, + and compliance for programs and projects. +status: draft +reasoning_mode: iterative +capabilities: + - Establish governance structures and stakeholder engagement plans + - Maintain comprehensive RAID and decision logs for executive visibility + - Ensure compliance with regulatory and organizational policy requirements +skills_available: + - artifact.create + - artifact.validate + - artifact.review +permissions: + - filesystem:read + - filesystem:write +artifact_metadata: + consumes: + - type: Program or project description + description: Input artifact of type Program or project description + - type: Stakeholders and governance structure + description: Input artifact of type Stakeholders and governance structure + - type: Objectives and success criteria + description: Input artifact of type Objectives and success criteria + - type: Compliance or regulatory requirements + description: Input artifact of type Compliance or regulatory requirements + - type: Risks, issues, and assumptions + description: Input artifact of type Risks, issues, and assumptions + - type: Decisions to be documented + description: Input artifact of type Decisions to be documented + produces: + - type: 'project-charter: Project charter with authority, scope, objectives, and + success criteria' + description: 'Output artifact of type project-charter: Project charter with authority, + scope, objectives, and success criteria' + - type: 'raid-log: Comprehensive RAID log' + description: 'Output artifact of type raid-log: Comprehensive RAID log' + - type: 'decision-log: Decision register with context, options, rationale, and outcomes' + description: 'Output artifact of type decision-log: Decision register with context, + options, rationale, and outcomes' + - type: 'governance-framework: Governance structure with roles, committees, and + decision rights' + description: 'Output artifact of type governance-framework: Governance structure + with roles, committees, and decision rights' + - type: 'compliance-matrix: Compliance mapping to regulatory and policy requirements' + description: 'Output artifact of type compliance-matrix: Compliance mapping to + regulatory and policy requirements' + - type: 'stakeholder-analysis: Stakeholder analysis with power/interest grid and + engagement strategy' + description: 'Output artifact of type stakeholder-analysis: Stakeholder analysis + with power/interest grid and engagement strategy' + - type: 'steering-committee-report: Executive steering committee reporting pack' + description: 'Output artifact of type steering-committee-report: Executive steering + committee reporting pack' + - type: 'change-control-process: Change management and approval workflow' + description: 'Output artifact of type change-control-process: Change management + and approval workflow' + - type: 'benefits-realization-plan: Benefits tracking and realization framework' + description: 'Output artifact of type benefits-realization-plan: Benefits tracking + and realization framework' diff --git a/agents/meta.agent/README.md b/agents/meta.agent/README.md new file mode 100644 index 0000000..700e48f --- /dev/null +++ b/agents/meta.agent/README.md @@ -0,0 +1,325 @@ +# meta.agent - Agent Creator + +The meta-agent that creates other agents through skill composition. + +## Overview + +**meta.agent** transforms natural language descriptions into complete, functional agents with proper skill composition, artifact metadata, and documentation. + +**What it produces:** +- Complete `agent.yaml` with recommended skills +- Auto-generated `README.md` documentation +- Proper artifact metadata (produces/consumes) +- Inferred permissions from skills + +## Quick Start + +### 1. Create an Agent Description + +Create a Markdown file describing your agent: + +```markdown +# Name: api.architect + +# Purpose: +An agent that designs comprehensive REST APIs and validates them +against best practices. + +# Inputs: +- API requirements + +# Outputs: +- openapi-spec +- validation-report +- api-models + +# Examples: +- Design a RESTful API for an e-commerce platform +- Create an API for a task management system +``` + +### 2. Run meta.agent + +```bash +python3 agents/meta.agent/meta_agent.py examples/api_architect_description.md +``` + +### 3. Output + +``` +✨ Agent 'api.architect' created successfully! + +📄 Agent definition: agents/api.architect/agent.yaml +📖 Documentation: agents/api.architect/README.md + +🔧 Skills: api.define, api.validate, workflow.validate +``` + +## Usage + +### Basic Creation + +```bash +# Create agent from Markdown description +python3 agents/meta.agent/meta_agent.py path/to/agent_description.md + +# Create agent from JSON description +python3 agents/meta.agent/meta_agent.py path/to/agent_description.json + +# Specify output directory +python3 agents/meta.agent/meta_agent.py description.md -o agents/my-agent + +# Skip validation +python3 agents/meta.agent/meta_agent.py description.md --no-validate +``` + +### Description Format + +**Markdown Format:** + +```markdown +# Name: agent-name + +# Purpose: +Detailed description of what the agent does... + +# Inputs: +- artifact-type-1 +- artifact-type-2 + +# Outputs: +- artifact-type-3 +- artifact-type-4 + +# Constraints: +(Optional) Any constraints or requirements... + +# Examples: +- Example use case 1 +- Example use case 2 +``` + +**JSON Format:** + +```json +{ + "name": "agent-name", + "purpose": "Detailed description...", + "inputs": ["artifact-type-1", "artifact-type-2"], + "outputs": ["artifact-type-3", "artifact-type-4"], + "examples": ["Example 1", "Example 2"] +} +``` + +## What meta.agent Creates + +### 1. agent.yaml + +Complete agent definition with: +- **Recommended skills** - Uses `agent.compose` to find compatible skills +- **Artifact metadata** - Proper produces/consumes declarations +- **Permissions** - Inferred from selected skills +- **Description** - Professional formatting + +Example output: +```yaml +name: api.architect +description: Designs and validates REST APIs against best practices +skills_available: + - api.define + - api.validate +permissions: + - filesystem:read + - filesystem:write +artifact_metadata: + consumes: + - type: api-requirements + produces: + - type: openapi-spec + schema: schemas/openapi-spec.json + - type: validation-report + schema: schemas/validation-report.json +``` + +### 2. README.md + +Auto-generated documentation with: +- Agent purpose and capabilities +- Skills used with rationale +- Artifact flow (inputs/outputs) +- Example use cases +- Usage instructions +- "Created by meta.agent" attribution + +## How It Works + +1. **Parse Description** - Reads Markdown or JSON +2. **Find Skills** - Uses `agent.compose` to recommend compatible skills +3. **Generate Metadata** - Uses `artifact.define` for artifact contracts +4. **Infer Permissions** - Analyzes required skills +5. **Create Files** - Generates agent.yaml and README.md +6. **Validate** - Ensures proper structure and compatibility + +## Integration with Other Meta-Agents + +### With meta.compatibility + +After creating an agent, use `meta.compatibility` to analyze it: + +```bash +# Create agent +python3 agents/meta.agent/meta_agent.py description.md + +# Analyze compatibility +python3 agents/meta.compatibility/meta_compatibility.py analyze api.architect +``` + +### With meta.suggest + +Get suggestions after creating an agent: + +```bash +python3 agents/meta.suggest/meta_suggest.py \ + --context meta.agent \ + --artifacts agents/api.architect/agent.yaml +``` + +## Common Workflows + +### Workflow 1: Create and Analyze + +```bash +# Step 1: Create agent +python3 agents/meta.agent/meta_agent.py examples/my_agent.md + +# Step 2: Analyze compatibility +python3 agents/meta.compatibility/meta_compatibility.py find-compatible my-agent + +# Step 3: Test the agent +# (Manual testing or agent.run) +``` + +### Workflow 2: Create Multiple Agents + +```bash +# Create several agents +for desc in examples/*_agent_description.md; do + python3 agents/meta.agent/meta_agent.py "$desc" +done + +# Analyze the ecosystem +python3 agents/meta.compatibility/meta_compatibility.py list-all +``` + +## Artifact Types + +### Consumes + +- **agent-description** - Natural language agent requirements + - Format: Markdown or JSON + - Pattern: `**/agent_description.md` + +### Produces + +- **agent-definition** - Complete agent.yaml + - Format: YAML + - Pattern: `agents/*/agent.yaml` + - Schema: `schemas/agent-definition.json` + +- **agent-documentation** - Auto-generated README + - Format: Markdown + - Pattern: `agents/*/README.md` + +## Tips & Best Practices + +### Writing Good Descriptions + +✅ **Good:** +- Clear, specific purpose +- Well-defined inputs and outputs +- Concrete examples +- Specific artifact types + +❌ **Avoid:** +- Vague purpose ("does stuff") +- Generic inputs ("data") +- No examples +- Unclear artifact types + +### Choosing Artifact Types + +Use existing artifact types when possible: +- `openapi-spec` for API specifications +- `validation-report` for validation results +- `workflow-definition` for workflows + +If you need a new type, create it with `meta.artifact` first. + +### Skill Selection + +meta.agent uses keyword matching to find skills: +- "api" → finds api.define, api.validate +- "validate" → finds validation skills +- "agent" → finds agent.compose, meta.agent + +Be descriptive in your purpose statement to get better skill recommendations. + +## Troubleshooting + +### Agent name conflicts + +``` +Error: Agent 'api.architect' already exists +``` + +**Solution:** Choose a different name or remove the existing agent directory. + +### No skills recommended + +``` +Warning: No skills found for agent purpose +``` + +**Solutions:** +- Make purpose more specific +- Mention artifact types explicitly +- Check if relevant skills exist in registry + +### Missing artifact types + +``` +Warning: Artifact type 'my-artifact' not in known registry +``` + +**Solution:** Create the artifact type with `meta.artifact` first: +```bash +python3 agents/meta.artifact/meta_artifact.py create artifact_description.md +``` + +## Examples + +See `examples/` directory for sample agent descriptions: +- `api_architect_description.md` - API design and validation agent +- (Add more as you create them) + +## Architecture + +meta.agent is part of the meta-agent ecosystem: + +``` +meta.agent + ├─ Uses: agent.compose (find skills) + ├─ Uses: artifact.define (generate metadata) + ├─ Produces: agent.yaml + README.md + └─ Works with: meta.compatibility, meta.suggest +``` + +## Related Documentation + +- [META_AGENTS.md](../../docs/META_AGENTS.md) - Complete meta-agent architecture +- [ARTIFACT_STANDARDS.md](../../docs/ARTIFACT_STANDARDS.md) - Artifact system +- [agent-description schema](../../schemas/agent-description.json) - JSON schema + +## Created By + +Part of the Betty Framework meta-agent ecosystem. diff --git a/agents/meta.agent/agent.yaml b/agents/meta.agent/agent.yaml new file mode 100644 index 0000000..499b7be --- /dev/null +++ b/agents/meta.agent/agent.yaml @@ -0,0 +1,93 @@ +name: meta.agent +version: 0.1.0 +description: | + Meta-agent that creates other agents by composing skills based on natural + language descriptions. Transforms natural language descriptions into complete, + functional agents. + + meta.agent analyzes agent requirements, recommends compatible skills using artifact + metadata, generates complete agent definitions, and produces documentation. + +artifact_metadata: + consumes: + - type: agent-description + file_pattern: "**/agent_description.md" + content_type: "text/markdown" + description: "Natural language description of agent purpose and requirements" + + produces: + - type: agent-definition + file_pattern: "agents/*/agent.yaml" + content_type: "application/yaml" + schema: "schemas/agent-definition.json" + description: "Complete agent configuration with skills and metadata" + + - type: agent-documentation + file_pattern: "agents/*/README.md" + content_type: "text/markdown" + description: "Human-readable agent documentation" + +status: draft +reasoning_mode: iterative +capabilities: + - Analyze agent requirements and identify compatible skills and capabilities + - Generate complete agent manifests, documentation, and supporting assets + - Validate registry consistency before registering new agents +skills_available: + - agent.compose # Find compatible skills based on requirements + - artifact.define # Generate artifact metadata for the new agent + - registry.update # Validate and register generated agents + +permissions: + - filesystem:read + - filesystem:write + +system_prompt: | + You are meta.agent, the meta-agent that creates other agents by composing skills. + + Your purpose is to transform natural language descriptions into complete, functional agents + with proper skill composition, artifact metadata, and documentation. + + ## Your Workflow + + 1. **Parse Requirements** - Understand what the agent needs to do + - Extract purpose, inputs, outputs, and constraints + - Identify required artifacts and permissions + + 2. **Compose Skills** - Use agent.compose to find compatible skills + - Analyze artifact flows (what's produced and consumed) + - Ensure no gaps in the artifact chain + - Consider permission requirements + + 3. **Generate Metadata** - Use artifact.define for proper artifact contracts + - Define what artifacts the agent consumes + - Define what artifacts the agent produces + - Include schemas and file patterns + + 4. **Create Agent Definition** - Write agent.yaml + - Name, description, skills_available + - Artifact metadata (consumes/produces) + - Permissions + - System prompt (optional but recommended) + + 5. **Document** - Generate comprehensive README.md + - Agent purpose and use cases + - Required inputs and expected outputs + - Example usage + - Artifact flow diagram + + 6. **Validate** (optional) - Use registry.certify + - Check agent definition is valid + - Verify skill compatibility + - Ensure artifact contracts are sound + + ## Principles + + - **Artifact-First Design**: Ensure clean artifact flows with no gaps + - **Minimal Skill Sets**: Only include skills the agent actually needs + - **Clear Documentation**: Make the agent's purpose immediately obvious + - **Convention Adherence**: Follow Betty Framework standards + - **Composability**: Design agents that work well with other agents + + When creating an agent, think like an architect: What does it consume? What does it + produce? What skills enable that transformation? How do artifacts flow through the system? diff --git a/agents/meta.agent/meta_agent.py b/agents/meta.agent/meta_agent.py new file mode 100755 index 0000000..cc84334 --- /dev/null +++ b/agents/meta.agent/meta_agent.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python3 +""" +meta.agent - Meta-agent that creates other agents + +Transforms natural language descriptions into complete, functional agents +by composing skills and generating proper artifact metadata. +""" + +import json +import yaml +import sys +import os +from pathlib import Path +from typing import Dict, List, Any, Optional + +# Add parent directory to path for imports +parent_dir = str(Path(__file__).parent.parent.parent) +sys.path.insert(0, parent_dir) + +# Import skill modules directly +agent_compose_path = Path(parent_dir) / "skills" / "agent.compose" +artifact_define_path = Path(parent_dir) / "skills" / "artifact.define" + +sys.path.insert(0, str(agent_compose_path)) +sys.path.insert(0, str(artifact_define_path)) + +import agent_compose +import artifact_define + +# Import traceability system +from betty.traceability import get_tracer, RequirementInfo + + +class AgentCreator: + """Creates agents from natural language descriptions""" + + def __init__(self, registry_path: str = "registry/skills.json"): + """Initialize with registry path""" + self.registry_path = Path(registry_path) + self.registry = self._load_registry() + + def _load_registry(self) -> Dict[str, Any]: + """Load skills registry""" + if not self.registry_path.exists(): + raise FileNotFoundError(f"Registry not found: {self.registry_path}") + + with open(self.registry_path) as f: + return json.load(f) + + def parse_description(self, description_path: str) -> Dict[str, Any]: + """ + Parse agent description from Markdown or JSON file + + Args: + description_path: Path to agent_description.md or agent_description.json + + Returns: + Parsed description with name, purpose, inputs, outputs, constraints + """ + path = Path(description_path) + + if not path.exists(): + raise FileNotFoundError(f"Description not found: {description_path}") + + # Handle JSON format + if path.suffix == ".json": + with open(path) as f: + return json.load(f) + + # Handle Markdown format + with open(path) as f: + content = f.read() + + # Parse Markdown sections + description = { + "name": "", + "purpose": "", + "inputs": [], + "outputs": [], + "constraints": {}, + "examples": [] + } + + current_section = None + for line in content.split('\n'): + line = line.strip() + + # Section headers + if line.startswith('# Name:'): + description["name"] = line.replace('# Name:', '').strip() + elif line.startswith('# Purpose:'): + current_section = "purpose" + elif line.startswith('# Inputs:'): + current_section = "inputs" + elif line.startswith('# Outputs:'): + current_section = "outputs" + elif line.startswith('# Constraints:'): + current_section = "constraints" + elif line.startswith('# Examples:'): + current_section = "examples" + elif line and not line.startswith('#'): + # Content for current section + if current_section == "purpose": + description["purpose"] += line + " " + elif current_section == "inputs" and line.startswith('-'): + # Extract artifact type (before parentheses or description) + artifact = line[1:].strip() + # Remove anything in parentheses and any extra description + if '(' in artifact: + artifact = artifact.split('(')[0].strip() + description["inputs"].append(artifact) + elif current_section == "outputs" and line.startswith('-'): + # Extract artifact type (before parentheses or description) + artifact = line[1:].strip() + # Remove anything in parentheses and any extra description + if '(' in artifact: + artifact = artifact.split('(')[0].strip() + description["outputs"].append(artifact) + elif current_section == "examples" and line.startswith('-'): + description["examples"].append(line[1:].strip()) + + description["purpose"] = description["purpose"].strip() + return description + + def find_compatible_skills( + self, + purpose: str, + required_artifacts: Optional[List[str]] = None + ) -> Dict[str, Any]: + """ + Find compatible skills for agent purpose + + Args: + purpose: Natural language description of agent purpose + required_artifacts: List of artifact types the agent needs + + Returns: + Dictionary with recommended skills and rationale + """ + return agent_compose.find_skills_for_purpose( + self.registry, + purpose, + required_artifacts + ) + + def generate_artifact_metadata( + self, + inputs: List[str], + outputs: List[str] + ) -> Dict[str, Any]: + """ + Generate artifact metadata from inputs/outputs + + Args: + inputs: List of input artifact types + outputs: List of output artifact types + + Returns: + Artifact metadata structure + """ + metadata = {} + + if inputs: + metadata["consumes"] = [] + for input_type in inputs: + artifact_def = artifact_define.get_artifact_definition(input_type) + if artifact_def: + metadata["consumes"].append(artifact_def) + else: + # Create basic definition + metadata["consumes"].append({ + "type": input_type, + "description": f"Input artifact of type {input_type}" + }) + + if outputs: + metadata["produces"] = [] + for output_type in outputs: + artifact_def = artifact_define.get_artifact_definition(output_type) + if artifact_def: + metadata["produces"].append(artifact_def) + else: + # Create basic definition + metadata["produces"].append({ + "type": output_type, + "description": f"Output artifact of type {output_type}" + }) + + return metadata + + def infer_permissions(self, skills: List[str]) -> List[str]: + """ + Infer required permissions from skills + + Args: + skills: List of skill names + + Returns: + List of required permissions + """ + permissions = set() + skills_list = self.registry.get("skills", []) + + for skill_name in skills: + # Find skill in registry + skill = next( + (s for s in skills_list if s.get("name") == skill_name), + None + ) + + if skill and "permissions" in skill: + for perm in skill["permissions"]: + permissions.add(perm) + + return sorted(list(permissions)) + + def generate_agent_yaml( + self, + name: str, + description: str, + skills: List[str], + artifact_metadata: Dict[str, Any], + permissions: List[str], + system_prompt: Optional[str] = None + ) -> str: + """ + Generate agent.yaml content + + Args: + name: Agent name + description: Agent description + skills: List of skill names + artifact_metadata: Artifact metadata structure + permissions: List of permissions + system_prompt: Optional system prompt + + Returns: + YAML content as string + """ + agent_def = { + "name": name, + "description": description, + "skills_available": skills, + "permissions": permissions + } + + if artifact_metadata: + agent_def["artifact_metadata"] = artifact_metadata + + if system_prompt: + agent_def["system_prompt"] = system_prompt + + return yaml.dump( + agent_def, + default_flow_style=False, + sort_keys=False, + allow_unicode=True + ) + + def generate_readme( + self, + name: str, + purpose: str, + skills: List[str], + inputs: List[str], + outputs: List[str], + examples: List[str] + ) -> str: + """ + Generate README.md content + + Args: + name: Agent name + purpose: Agent purpose + skills: List of skill names + inputs: Input artifacts + outputs: Output artifacts + examples: Example use cases + + Returns: + Markdown content + """ + readme = f"""# {name.title()} Agent + +## Purpose + +{purpose} + +## Skills + +This agent uses the following skills: + +""" + for skill in skills: + readme += f"- `{skill}`\n" + + if inputs or outputs: + readme += "\n## Artifact Flow\n\n" + + if inputs: + readme += "### Consumes\n\n" + for inp in inputs: + readme += f"- `{inp}`\n" + readme += "\n" + + if outputs: + readme += "### Produces\n\n" + for out in outputs: + readme += f"- `{out}`\n" + readme += "\n" + + if examples: + readme += "## Example Use Cases\n\n" + for example in examples: + readme += f"- {example}\n" + readme += "\n" + + readme += """## Usage + +```bash +# Activate the agent +/agent {name} + +# Or invoke directly +betty agent run {name} --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* +""".format(name=name) + + return readme + + def create_agent( + self, + description_path: str, + output_dir: Optional[str] = None, + validate: bool = True, + requirement: Optional[RequirementInfo] = None + ) -> Dict[str, str]: + """ + Create a complete agent from description + + Args: + description_path: Path to agent description file + output_dir: Output directory (default: agents/{name}/) + validate: Whether to validate with registry.certify + requirement: Requirement information for traceability (optional) + + Returns: + Dictionary with paths to created files + """ + # Parse description + desc = self.parse_description(description_path) + name = desc["name"] + + if not name: + raise ValueError("Agent name is required") + + # Determine output directory + if not output_dir: + output_dir = f"agents/{name}" + + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + # Find compatible skills + skill_recommendations = self.find_compatible_skills( + desc["purpose"], + desc.get("inputs", []) + desc.get("outputs", []) + ) + + skills = skill_recommendations.get("recommended_skills", []) + + # Generate artifact metadata + artifact_metadata = self.generate_artifact_metadata( + desc.get("inputs", []), + desc.get("outputs", []) + ) + + # Infer permissions + permissions = self.infer_permissions(skills) + + # Generate agent.yaml + agent_yaml_content = self.generate_agent_yaml( + name=name, + description=desc["purpose"], + skills=skills, + artifact_metadata=artifact_metadata, + permissions=permissions + ) + + agent_yaml_path = output_path / "agent.yaml" + with open(agent_yaml_path, 'w') as f: + f.write(agent_yaml_content) + + # Generate README.md + readme_content = self.generate_readme( + name=name, + purpose=desc["purpose"], + skills=skills, + inputs=desc.get("inputs", []), + outputs=desc.get("outputs", []), + examples=desc.get("examples", []) + ) + + readme_path = output_path / "README.md" + with open(readme_path, 'w') as f: + f.write(readme_content) + + # Log traceability if requirement provided + trace_id = None + if requirement: + try: + tracer = get_tracer() + trace_id = tracer.log_creation( + component_id=name, + component_name=name.replace(".", " ").title(), + component_type="agent", + component_version="0.1.0", + component_file_path=str(agent_yaml_path), + input_source_path=description_path, + created_by_tool="meta.agent", + created_by_version="0.1.0", + requirement=requirement, + tags=["agent", "auto-generated"], + project="Betty Framework" + ) + + # Log validation check + tracer.log_verification( + component_id=name, + check_type="validation", + tool="meta.agent", + result="passed", + details={ + "checks_performed": [ + {"name": "agent_structure", "status": "passed"}, + {"name": "artifact_metadata", "status": "passed"}, + {"name": "skills_compatibility", "status": "passed", "message": f"{len(skills)} compatible skills found"} + ] + } + ) + except Exception as e: + print(f"⚠️ Warning: Could not log traceability: {e}") + + result = { + "agent_yaml": str(agent_yaml_path), + "readme": str(readme_path), + "name": name, + "skills": skills, + "rationale": skill_recommendations.get("rationale", "") + } + + if trace_id: + result["trace_id"] = trace_id + + return result + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="meta.agent - Create agents from natural language descriptions" + ) + parser.add_argument( + "description", + help="Path to agent description file (.md or .json)" + ) + parser.add_argument( + "-o", "--output", + help="Output directory (default: agents/{name}/)" + ) + parser.add_argument( + "--no-validate", + action="store_true", + help="Skip validation step" + ) + + # Traceability arguments + parser.add_argument( + "--requirement-id", + help="Requirement identifier for traceability (e.g., REQ-2025-001)" + ) + parser.add_argument( + "--requirement-description", + help="What this agent is meant to accomplish" + ) + parser.add_argument( + "--requirement-source", + help="Source document or system (e.g., requirements/Q1-2025.md)" + ) + parser.add_argument( + "--issue-id", + help="Issue tracking ID (e.g., JIRA-123)" + ) + parser.add_argument( + "--requested-by", + help="Who requested this requirement" + ) + parser.add_argument( + "--rationale", + help="Why this component is needed" + ) + + args = parser.parse_args() + + # Create requirement info if provided + requirement = None + if args.requirement_id and args.requirement_description: + requirement = RequirementInfo( + id=args.requirement_id, + description=args.requirement_description, + source=args.requirement_source, + issue_id=args.issue_id, + requested_by=args.requested_by, + rationale=args.rationale + ) + + # Create agent + creator = AgentCreator() + + print(f"🔮 meta.agent creating agent from {args.description}...") + + try: + result = creator.create_agent( + args.description, + output_dir=args.output, + validate=not args.no_validate, + requirement=requirement + ) + + print(f"\n✨ Agent '{result['name']}' created successfully!\n") + print(f"📄 Agent definition: {result['agent_yaml']}") + print(f"📖 Documentation: {result['readme']}\n") + print(f"🔧 Skills: {', '.join(result['skills'])}\n") + + if result.get("rationale"): + print(f"💡 Rationale:\n{result['rationale']}\n") + + if result.get("trace_id"): + print(f"📝 Traceability: {result['trace_id']}") + print(f" View trace: python3 betty/trace_cli.py show {result['name']}\n") + + except Exception as e: + print(f"\n❌ Error creating agent: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/agents/meta.artifact/README.md b/agents/meta.artifact/README.md new file mode 100644 index 0000000..de73a2b --- /dev/null +++ b/agents/meta.artifact/README.md @@ -0,0 +1,372 @@ +# meta.artifact - The Artifact Standards Authority + +THE single source of truth for all artifact type definitions in Betty Framework. + +## Overview + +**meta.artifact** manages the complete lifecycle of artifact types - from definition to documentation to registration. All artifact types MUST be created through meta.artifact. No ad-hoc definitions are permitted. + +**What it does:** +- Defines new artifact types from descriptions +- Generates JSON schemas with validation rules +- Updates ARTIFACT_STANDARDS.md automatically +- Registers types in KNOWN_ARTIFACT_TYPES +- Validates uniqueness and prevents conflicts + +## Quick Start + +### 1. Create Artifact Description + +```markdown +# Name: optimization-report + +# Purpose: +Performance and security optimization recommendations for APIs + +# Format: JSON + +# File Pattern: *.optimization.json + +# Schema Properties: +- optimizations (array): List of optimization recommendations +- severity (string): Severity level +- analyzed_artifact (string): Reference to analyzed artifact + +# Required Fields: +- optimizations +- severity +- analyzed_artifact + +# Producers: +- api.optimize + +# Consumers: +- api.implement +- report.generate +``` + +### 2. Create Artifact Type + +```bash +python3 agents/meta.artifact/meta_artifact.py create examples/optimization_report_artifact.md +``` + +### 3. Output + +``` +✨ Artifact type 'optimization-report' created successfully! + +📄 Created files: + - schemas/optimization-report.json + +📝 Updated files: + - docs/ARTIFACT_STANDARDS.md + - skills/artifact.define/artifact_define.py + +✅ Artifact type 'optimization-report' is now registered +``` + +## Usage + +### Create New Artifact Type + +```bash +# From Markdown description +python3 agents/meta.artifact/meta_artifact.py create artifact_description.md + +# From JSON description +python3 agents/meta.artifact/meta_artifact.py create artifact_description.json + +# Force overwrite if exists +python3 agents/meta.artifact/meta_artifact.py create artifact_description.md --force +``` + +### Check if Artifact Exists + +```bash +python3 agents/meta.artifact/meta_artifact.py check optimization-report +``` + +Output: +``` +✅ Artifact type 'optimization-report' exists + Location: docs/ARTIFACT_STANDARDS.md +``` + +## What meta.artifact Creates + +### 1. JSON Schema (schemas/*.json) + +Complete JSON Schema Draft 07 schema with: +- Properties from description +- Required fields +- Type validation +- Descriptions + +Example: +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Optimization Report", + "description": "Performance and security recommendations...", + "type": "object", + "required": ["optimizations", "severity", "analyzed_artifact"], + "properties": { + "optimizations": { + "type": "array", + "description": "List of optimization recommendations" + }, + ... + } +} +``` + +### 2. Documentation Section (docs/ARTIFACT_STANDARDS.md) + +Adds complete section with: +- Artifact number and title +- Description +- Convention (file pattern, format, content type) +- Schema reference +- Producers and consumers +- Related types + +### 3. Registry Entry (skills/artifact.define/artifact_define.py) + +Adds to KNOWN_ARTIFACT_TYPES: +```python +"optimization-report": { + "schema": "schemas/optimization-report.json", + "file_pattern": "*.optimization.json", + "content_type": "application/json", + "description": "Performance and security optimization recommendations..." +} +``` + +## Description Format + +### Markdown Format + +```markdown +# Name: artifact-type-name + +# Purpose: +Detailed description of what this artifact represents... + +# Format: JSON | YAML | Markdown | Python | etc. + +# File Pattern: *.artifact-type.ext + +# Content Type: application/json (optional, inferred from format) + +# Schema Properties: +- property_name (type): Description +- another_property (array): Description + +# Required Fields: +- property_name +- another_property + +# Producers: +- skill.that.produces +- agent.that.produces + +# Consumers: +- skill.that.consumes +- agent.that.consumes + +# Related Types: +- related-artifact-1 +- related-artifact-2 + +# Validation Rules: +- Custom rule 1 +- Custom rule 2 +``` + +### JSON Format + +```json +{ + "name": "artifact-type-name", + "purpose": "Description...", + "format": "JSON", + "file_pattern": "*.artifact-type.json", + "schema_properties": { + "field1": {"type": "string", "description": "..."}, + "field2": {"type": "array", "description": "..."} + }, + "required_fields": ["field1"], + "producers": ["producer.skill"], + "consumers": ["consumer.skill"] +} +``` + +## Governance Rules + +meta.artifact enforces these rules: + +1. **Uniqueness** - Each artifact type must have a unique name +2. **Clarity** - Names must be descriptive (e.g., "openapi-spec" not "spec") +3. **Consistency** - Must use kebab-case (lowercase with hyphens) +4. **Documentation** - Every type must be fully documented +5. **Schemas** - Every type should have a JSON schema (if applicable) +6. **No Conflicts** - Checks for naming conflicts before creating + +## Workflow + +``` +Developer creates artifact_description.md + ↓ +meta.artifact validates name and format + ↓ +Checks if type already exists + ↓ +Generates JSON Schema + ↓ +Updates ARTIFACT_STANDARDS.md + ↓ +Adds to KNOWN_ARTIFACT_TYPES + ↓ +Validates all files + ↓ +Type is now registered and usable +``` + +## Integration + +### With meta.agent + +When meta.agent needs a new artifact type: + +```bash +# 1. Define the artifact type +python3 agents/meta.artifact/meta_artifact.py create my_artifact.md + +# 2. Create agent that uses it +python3 agents/meta.agent/meta_agent.py agent_description.md +``` + +### With meta.suggest + +meta.suggest will recommend creating artifact types for gaps: + +```bash +python3 agents/meta.suggest/meta_suggest.py --analyze-project +``` + +Output includes: +``` +💡 Suggestions: + 1. Create agent/skill to produce 'missing-artifact' +``` + +## Existing Artifact Types + +Check `docs/ARTIFACT_STANDARDS.md` for all registered types: + +- `openapi-spec` - OpenAPI specifications +- `validation-report` - Validation results +- `workflow-definition` - Betty workflows +- `hook-config` - Claude Code hooks +- `api-models` - Generated data models +- `agent-description` - Agent requirements +- `agent-definition` - Agent configurations +- `agent-documentation` - Agent READMEs +- `optimization-report` - Optimization recommendations +- `compatibility-graph` - Agent relationships +- `pipeline-suggestion` - Multi-agent workflows +- `suggestion-report` - Next-step recommendations + +## Tips & Best Practices + +### Naming Artifact Types + +✅ **Good:** +- `validation-report` (clear, descriptive) +- `openapi-spec` (standard term) +- `optimization-report` (action + result) + +❌ **Avoid:** +- `report` (too generic) +- `validationReport` (should be kebab-case) +- `val-rep` (abbreviations) + +### Writing Descriptions + +Be comprehensive: +- Explain what the artifact represents +- Include all important properties +- Document producers and consumers +- Add related types for discoverability + +### Schema Properties + +Be specific about types: +- Use JSON Schema types: string, number, integer, boolean, array, object +- Add descriptions for every property +- Mark required fields +- Consider validation rules + +## Troubleshooting + +### Name already exists + +``` +Error: Artifact type 'my-artifact' already exists at: docs/ARTIFACT_STANDARDS.md +``` + +**Solutions:** +1. Use `--force` to overwrite (careful!) +2. Choose a different name +3. Use the existing type if appropriate + +### Invalid name format + +``` +Error: Artifact name must be kebab-case (lowercase with hyphens): MyArtifact +``` + +**Solution:** Use lowercase with hyphens: `my-artifact` + +### Missing schema properties + +If your artifact is JSON/YAML but has no schema properties, meta.artifact will still create a basic schema. Add properties for better validation. + +## Architecture + +meta.artifact is THE authority in the meta-agent ecosystem: + +``` +meta.artifact (Authority) + ├─ Manages: All artifact type definitions + ├─ Updates: ARTIFACT_STANDARDS.md + ├─ Registers: KNOWN_ARTIFACT_TYPES + ├─ Used by: meta.agent, meta.skill, all agents + └─ Governance: Single source of truth +``` + +## Examples + +See `examples/` for artifact descriptions: +- `optimization_report_artifact.md` +- `compatibility_graph_artifact.md` +- `pipeline_suggestion_artifact.md` +- `suggestion_report_artifact.md` + +## Related Documentation + +- [ARTIFACT_STANDARDS.md](../../docs/ARTIFACT_STANDARDS.md) - Complete artifact documentation +- [artifact-type-description schema](../../schemas/artifact-type-description.json) +- [META_AGENTS.md](../../docs/META_AGENTS.md) - Meta-agent ecosystem + +## Philosophy + +**Single Source of Truth** - All artifact definitions flow through meta.artifact. This ensures: +- Consistency across the framework +- Proper documentation +- Schema validation +- No conflicts +- Discoverability + +When in doubt, ask meta.artifact. diff --git a/agents/meta.artifact/agent.yaml b/agents/meta.artifact/agent.yaml new file mode 100644 index 0000000..21f8569 --- /dev/null +++ b/agents/meta.artifact/agent.yaml @@ -0,0 +1,144 @@ +name: meta.artifact +version: 0.1.0 +description: | + The artifact standards authority - THE single source of truth for all + artifact type definitions in Betty Framework. + + This meta-agent manages the complete lifecycle of artifact types: + - Defines new artifact types with JSON schemas + - Updates ARTIFACT_STANDARDS.md documentation + - Registers types in the artifact registry + - Validates artifact compatibility across the system + - Ensures consistency and prevents conflicts + + All artifact types MUST be registered through meta.artifact before use. + No ad-hoc artifact definitions are permitted. + +artifact_metadata: + consumes: + - type: artifact-type-description + file_pattern: "**/artifact_type_description.md" + content_type: "text/markdown" + description: "Natural language description of a new artifact type" + schema: "schemas/artifact-type-description.json" + + produces: + - type: artifact-schema + file_pattern: "schemas/*.json" + content_type: "application/json" + schema: "http://json-schema.org/draft-07/schema#" + description: "JSON Schema for validating artifact instances" + + - type: artifact-documentation + file_pattern: "docs/ARTIFACT_STANDARDS.md" + content_type: "text/markdown" + description: "Updated artifact standards documentation" + + - type: artifact-registry-entry + file_pattern: "skills/artifact.define/artifact_define.py" + content_type: "text/x-python" + description: "Updated KNOWN_ARTIFACT_TYPES registry" + +status: draft +reasoning_mode: iterative +capabilities: + - Curate and register canonical artifact type definitions and schemas + - Synchronize documentation with changes to artifact standards + - Validate artifact compatibility across registries and manifests +skills_available: + - artifact.define # Use existing artifact definitions + - registry.update # Register or amend artifact metadata + - registry.query # Inspect existing registry entries + +permissions: + - filesystem:read + - filesystem:write + +system_prompt: | + You are meta.artifact, the artifact standards authority for Betty Framework. + + You are THE single source of truth for artifact type definitions. All artifact + types flow through you - no exceptions. + + ## Your Responsibilities + + 1. **Define New Artifact Types** + - Parse artifact type descriptions + - Validate uniqueness (check if type already exists) + - Create JSON schemas with proper validation rules + - Generate comprehensive documentation + - Register in KNOWN_ARTIFACT_TYPES + + 2. **Maintain Standards Documentation** + - Update docs/ARTIFACT_STANDARDS.md with new types + - Include file patterns, schemas, producers, consumers + - Provide clear examples + - Keep Quick Reference table up to date + + 3. **Validate Compatibility** + - Check if artifact types can work together + - Verify producer/consumer contracts + - Ensure no naming conflicts + - Validate schema consistency + + 4. **Registry Management** + - Update skills/artifact.define/artifact_define.py + - Add to KNOWN_ARTIFACT_TYPES dictionary + - Include all metadata (schema, file_pattern, content_type, description) + + ## Workflow for New Artifact Type + + 1. **Check Existence** + - Search ARTIFACT_STANDARDS.md for similar types + - Check KNOWN_ARTIFACT_TYPES registry + - Suggest existing type if appropriate + + 2. **Generate JSON Schema** + - Create schemas/{type-name}.json + - Include proper validation rules + - Use JSON Schema Draft 07 + - Add description, examples, required fields + + 3. **Update Documentation** + - Add new section to ARTIFACT_STANDARDS.md + - Follow existing format (Description, Convention, Schema, Producers, Consumers) + - Update Quick Reference table + + 4. **Update Registry** + - Add entry to KNOWN_ARTIFACT_TYPES in artifact_define.py + - Include: schema, file_pattern, content_type, description + + 5. **Validate** + - Ensure all files are properly formatted + - Check for syntax errors + - Validate schema is valid JSON Schema + + ## Governance Rules + + - **Uniqueness**: Each artifact type must have a unique name + - **Clarity**: Names should be descriptive (e.g., "openapi-spec" not "spec") + - **Consistency**: Follow kebab-case naming (lowercase with hyphens) + - **Documentation**: Every type must be fully documented + - **Schemas**: Every type should have a JSON schema (if applicable) + - **No Conflicts**: Check for naming conflicts before creating + + ## Example Workflow + + User provides artifact_type_description.md: + ``` + # Name: optimization-report + # Purpose: API optimization recommendations + # Format: JSON + # Producers: api.optimize + # Consumers: api.implement + ``` + + You: + 1. Check if "optimization-report" exists → it doesn't + 2. Generate schemas/optimization-report.json + 3. Update ARTIFACT_STANDARDS.md with new section + 4. Add to KNOWN_ARTIFACT_TYPES + 5. Return summary of changes + + Remember: You are the guardian of artifact standards. Be thorough, be consistent, + be the single source of truth. diff --git a/agents/meta.artifact/meta_artifact.py b/agents/meta.artifact/meta_artifact.py new file mode 100755 index 0000000..d45b345 --- /dev/null +++ b/agents/meta.artifact/meta_artifact.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +""" +meta.artifact - The Artifact Standards Authority + +THE single source of truth for all artifact type definitions in Betty Framework. +Manages schemas, documentation, and registry for all artifact types. +""" + +import json +import yaml +import sys +import os +import re +from pathlib import Path +from typing import Dict, List, Any, Optional, Tuple +from datetime import datetime + +# Add parent directory to path for imports +parent_dir = str(Path(__file__).parent.parent.parent) +sys.path.insert(0, parent_dir) + + +class ArtifactAuthority: + """The artifact standards authority - manages all artifact type definitions""" + + def __init__(self, base_dir: str = "."): + """Initialize with base directory""" + self.base_dir = Path(base_dir) + self.standards_doc = self.base_dir / "docs" / "ARTIFACT_STANDARDS.md" + self.schemas_dir = self.base_dir / "schemas" + self.artifact_define = self.base_dir / "skills" / "artifact.define" / "artifact_define.py" + + def parse_description(self, description_path: str) -> Dict[str, Any]: + """ + Parse artifact type description from Markdown or JSON file + + Args: + description_path: Path to artifact_type_description.md or .json + + Returns: + Parsed description with all artifact metadata + """ + path = Path(description_path) + + if not path.exists(): + raise FileNotFoundError(f"Description not found: {description_path}") + + # Handle JSON format + if path.suffix == ".json": + with open(path) as f: + return json.load(f) + + # Handle Markdown format + with open(path) as f: + content = f.read() + + # Parse Markdown sections + description = { + "name": "", + "purpose": "", + "format": "", + "file_pattern": "", + "content_type": "", + "schema_properties": {}, + "required_fields": [], + "producers": [], + "consumers": [], + "examples": [], + "validation_rules": [], + "related_types": [] + } + + current_section = None + for line in content.split('\n'): + line = line.strip() + + # Section headers + if line.startswith('# Name:'): + description["name"] = line.replace('# Name:', '').strip() + elif line.startswith('# Purpose:'): + current_section = "purpose" + elif line.startswith('# Format:'): + description["format"] = line.replace('# Format:', '').strip() + elif line.startswith('# File Pattern:'): + description["file_pattern"] = line.replace('# File Pattern:', '').strip() + elif line.startswith('# Content Type:'): + description["content_type"] = line.replace('# Content Type:', '').strip() + elif line.startswith('# Schema Properties:'): + current_section = "schema_properties" + elif line.startswith('# Required Fields:'): + current_section = "required_fields" + elif line.startswith('# Producers:'): + current_section = "producers" + elif line.startswith('# Consumers:'): + current_section = "consumers" + elif line.startswith('# Examples:'): + current_section = "examples" + elif line.startswith('# Validation Rules:'): + current_section = "validation_rules" + elif line.startswith('# Related Types:'): + current_section = "related_types" + elif line and not line.startswith('#'): + # Content for current section + if current_section == "purpose": + description["purpose"] += line + " " + elif current_section in ["producers", "consumers", "required_fields", + "validation_rules", "related_types"] and line.startswith('-'): + description[current_section].append(line[1:].strip()) + elif current_section == "schema_properties" and line.startswith('-'): + # Parse property definitions like: "- optimizations (array): List of optimizations" + match = re.match(r'-\s+(\w+)\s+\((\w+)\):\s*(.+)', line) + if match: + prop_name, prop_type, prop_desc = match.groups() + description["schema_properties"][prop_name] = { + "type": prop_type, + "description": prop_desc + } + + description["purpose"] = description["purpose"].strip() + + # Infer content_type from format if not specified + if not description["content_type"] and description["format"]: + format_to_mime = { + "JSON": "application/json", + "YAML": "application/yaml", + "Markdown": "text/markdown", + "Python": "text/x-python", + "TypeScript": "text/x-typescript", + "Go": "text/x-go", + "Text": "text/plain" + } + description["content_type"] = format_to_mime.get(description["format"], "") + + return description + + def check_existence(self, artifact_name: str) -> Tuple[bool, Optional[str]]: + """ + Check if artifact type already exists + + Args: + artifact_name: Name of artifact type to check + + Returns: + Tuple of (exists: bool, location: Optional[str]) + """ + # Check in ARTIFACT_STANDARDS.md + if self.standards_doc.exists(): + with open(self.standards_doc) as f: + content = f.read() + if f"`{artifact_name}`" in content or f"({artifact_name})" in content: + return True, str(self.standards_doc) + + # Check in schemas directory + schema_file = self.schemas_dir / f"{artifact_name}.json" + if schema_file.exists(): + return True, str(schema_file) + + # Check in KNOWN_ARTIFACT_TYPES + if self.artifact_define.exists(): + with open(self.artifact_define) as f: + content = f.read() + if f'"{artifact_name}"' in content: + return True, str(self.artifact_define) + + return False, None + + def generate_json_schema( + self, + artifact_desc: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Generate JSON Schema from artifact description + + Args: + artifact_desc: Parsed artifact description + + Returns: + JSON Schema dictionary + """ + schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": artifact_desc["name"].replace("-", " ").title(), + "description": artifact_desc["purpose"], + "type": "object" + } + + # Add required fields + if artifact_desc.get("required_fields"): + schema["required"] = artifact_desc["required_fields"] + + # Add properties from schema_properties + if artifact_desc.get("schema_properties"): + schema["properties"] = {} + for prop_name, prop_info in artifact_desc["schema_properties"].items(): + prop_schema = {} + + # Map simple types to JSON Schema types + type_mapping = { + "string": "string", + "number": "number", + "integer": "integer", + "boolean": "boolean", + "array": "array", + "object": "object" + } + + prop_type = prop_info.get("type", "string").lower() + prop_schema["type"] = type_mapping.get(prop_type, "string") + + if "description" in prop_info: + prop_schema["description"] = prop_info["description"] + + schema["properties"][prop_name] = prop_schema + + # Add examples if provided + if artifact_desc.get("examples"): + schema["examples"] = artifact_desc["examples"] + + return schema + + def update_standards_doc(self, artifact_desc: Dict[str, Any]) -> None: + """ + Update ARTIFACT_STANDARDS.md with new artifact type + + Args: + artifact_desc: Parsed artifact description + """ + if not self.standards_doc.exists(): + raise FileNotFoundError(f"Standards document not found: {self.standards_doc}") + + with open(self.standards_doc) as f: + content = f.read() + + # Find the "## Artifact Types" section + artifact_types_match = re.search(r'## Artifact Types\n', content) + if not artifact_types_match: + raise ValueError("Could not find '## Artifact Types' section in standards doc") + + # Find where to insert (before "## Artifact Metadata Schema" or at end) + insert_before_match = re.search(r'\n## Artifact Metadata Schema\n', content) + + # Generate new section + artifact_name = artifact_desc["name"] + section_number = self._get_next_artifact_number(content) + + new_section = f""" +### {section_number}. {artifact_name.replace('-', ' ').title()} (`{artifact_name}`) + +**Description:** {artifact_desc["purpose"]} + +**Convention:** +- File pattern: `{artifact_desc.get("file_pattern", f"*.{artifact_name}.{artifact_desc['format'].lower()}")}` +- Format: {artifact_desc["format"]} +""" + + if artifact_desc.get("content_type"): + new_section += f"- Content type: {artifact_desc['content_type']}\n" + + if artifact_desc.get("schema_properties"): + new_section += f"\n**Schema:** `schemas/{artifact_name}.json`\n" + + if artifact_desc.get("producers"): + new_section += "\n**Produced by:**\n" + for producer in artifact_desc["producers"]: + new_section += f"- `{producer}`\n" + + if artifact_desc.get("consumers"): + new_section += "\n**Consumed by:**\n" + for consumer in artifact_desc["consumers"]: + new_section += f"- `{consumer}`\n" + + if artifact_desc.get("related_types"): + new_section += "\n**Related types:**\n" + for related in artifact_desc["related_types"]: + new_section += f"- `{related}`\n" + + new_section += "\n---\n" + + # Insert the new section + if insert_before_match: + insert_pos = insert_before_match.start() + else: + insert_pos = len(content) + + updated_content = content[:insert_pos] + new_section + content[insert_pos:] + + # Update Quick Reference table + updated_content = self._update_quick_reference(updated_content, artifact_desc) + + # Write back + with open(self.standards_doc, 'w') as f: + f.write(updated_content) + + def _get_next_artifact_number(self, standards_content: str) -> int: + """Get the next artifact type number for documentation""" + # Find all artifact type sections like "### 1. ", "### 2. ", etc. + matches = re.findall(r'### (\d+)\. .+? \(`[\w-]+`\)', standards_content) + if matches: + return max(int(m) for m in matches) + 1 + return 1 + + def _update_quick_reference( + self, + content: str, + artifact_desc: Dict[str, Any] + ) -> str: + """Update the Quick Reference table with new artifact type""" + # Find the Quick Reference table + table_match = re.search( + r'\| Artifact Type \| File Pattern \| Schema \| Producers \| Consumers \|.*?\n\|.*?\n((?:\|.*?\n)*)', + content, + re.DOTALL + ) + + if not table_match: + return content + + artifact_name = artifact_desc["name"] + file_pattern = artifact_desc.get("file_pattern", f"*.{artifact_name}.{artifact_desc['format'].lower()}") + schema = f"schemas/{artifact_name}.json" if artifact_desc.get("schema_properties") else "-" + producers = ", ".join(artifact_desc.get("producers", [])) or "-" + consumers = ", ".join(artifact_desc.get("consumers", [])) or "-" + + new_row = f"| {artifact_name} | {file_pattern} | {schema} | {producers} | {consumers} |\n" + + # Insert before the end of the table + table_end = table_match.end() + return content[:table_end] + new_row + content[table_end:] + + def update_registry(self, artifact_desc: Dict[str, Any]) -> None: + """ + Update KNOWN_ARTIFACT_TYPES in artifact_define.py + + Args: + artifact_desc: Parsed artifact description + """ + if not self.artifact_define.exists(): + raise FileNotFoundError(f"Artifact registry not found: {self.artifact_define}") + + with open(self.artifact_define) as f: + content = f.read() + + # Find KNOWN_ARTIFACT_TYPES dictionary + match = re.search(r'KNOWN_ARTIFACT_TYPES = \{', content) + if not match: + raise ValueError("Could not find KNOWN_ARTIFACT_TYPES in artifact_define.py") + + artifact_name = artifact_desc["name"] + + # Generate new entry + entry = f' "{artifact_name}": {{\n' + + if artifact_desc.get("schema_properties"): + entry += f' "schema": "schemas/{artifact_name}.json",\n' + + file_pattern = artifact_desc.get("file_pattern") + if file_pattern: + entry += f' "file_pattern": "{file_pattern}",\n' + + if artifact_desc.get("content_type"): + entry += f' "content_type": "{artifact_desc["content_type"]}",\n' + + entry += f' "description": "{artifact_desc["purpose"]}"\n' + entry += ' },\n' + + # Find the end of the dictionary (last closing brace before the closing of KNOWN_ARTIFACT_TYPES) + # Insert before the last } + closing_brace_match = list(re.finditer(r'\n\}', content)) + if closing_brace_match: + # Find the one that's part of KNOWN_ARTIFACT_TYPES + # This is a bit tricky, but we'll insert before the last } in the KNOWN_ARTIFACT_TYPES section + insert_pos = closing_brace_match[0].start() + + # Insert the new entry + updated_content = content[:insert_pos] + entry + content[insert_pos:] + + # Write back + with open(self.artifact_define, 'w') as f: + f.write(updated_content) + + def create_artifact_type( + self, + description_path: str, + force: bool = False + ) -> Dict[str, Any]: + """ + Create a new artifact type from description + + Args: + description_path: Path to artifact description file + force: Force creation even if type exists + + Returns: + Summary of created files and changes + """ + # Parse description + artifact_desc = self.parse_description(description_path) + artifact_name = artifact_desc["name"] + + # Validate name format (kebab-case) + if not re.match(r'^[a-z0-9-]+$', artifact_name): + raise ValueError( + f"Artifact name must be kebab-case (lowercase with hyphens): {artifact_name}" + ) + + # Check existence + exists, location = self.check_existence(artifact_name) + if exists and not force: + raise ValueError( + f"Artifact type '{artifact_name}' already exists at: {location}\n" + f"Use --force to overwrite." + ) + + result = { + "artifact_name": artifact_name, + "created_files": [], + "updated_files": [], + "errors": [] + } + + # Generate and save JSON schema (if applicable) + if artifact_desc.get("schema_properties") or artifact_desc["format"] in ["JSON", "YAML"]: + schema = self.generate_json_schema(artifact_desc) + schema_file = self.schemas_dir / f"{artifact_name}.json" + + self.schemas_dir.mkdir(parents=True, exist_ok=True) + with open(schema_file, 'w') as f: + json.dump(schema, f, indent=2) + + result["created_files"].append(str(schema_file)) + + # Update ARTIFACT_STANDARDS.md + try: + self.update_standards_doc(artifact_desc) + result["updated_files"].append(str(self.standards_doc)) + except Exception as e: + result["errors"].append(f"Failed to update standards doc: {e}") + + # Update artifact registry + try: + self.update_registry(artifact_desc) + result["updated_files"].append(str(self.artifact_define)) + except Exception as e: + result["errors"].append(f"Failed to update registry: {e}") + + return result + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="meta.artifact - The Artifact Standards Authority" + ) + + subparsers = parser.add_subparsers(dest='command', help='Commands') + + # Create command + create_parser = subparsers.add_parser('create', help='Create new artifact type') + create_parser.add_argument( + "description", + help="Path to artifact type description file (.md or .json)" + ) + create_parser.add_argument( + "--force", + action="store_true", + help="Force creation even if type exists" + ) + + # Check command + check_parser = subparsers.add_parser('check', help='Check if artifact type exists') + check_parser.add_argument("name", help="Artifact type name") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + authority = ArtifactAuthority() + + if args.command == 'create': + print(f"🏛️ meta.artifact - Creating artifact type from {args.description}") + + try: + result = authority.create_artifact_type(args.description, force=args.force) + + print(f"\n✨ Artifact type '{result['artifact_name']}' created successfully!\n") + + if result["created_files"]: + print("📄 Created files:") + for file in result["created_files"]: + print(f" - {file}") + + if result["updated_files"]: + print("\n📝 Updated files:") + for file in result["updated_files"]: + print(f" - {file}") + + if result["errors"]: + print("\n⚠️ Warnings:") + for error in result["errors"]: + print(f" - {error}") + + print(f"\n✅ Artifact type '{result['artifact_name']}' is now registered") + print(" All agents and skills can now use this artifact type.") + + except Exception as e: + print(f"\n❌ Error creating artifact type: {e}", file=sys.stderr) + sys.exit(1) + + elif args.command == 'check': + exists, location = authority.check_existence(args.name) + + if exists: + print(f"✅ Artifact type '{args.name}' exists") + print(f" Location: {location}") + else: + print(f"❌ Artifact type '{args.name}' does not exist") + print(f" Use 'meta.artifact create' to define it") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/agents/meta.command/README.md b/agents/meta.command/README.md new file mode 100644 index 0000000..58791c9 --- /dev/null +++ b/agents/meta.command/README.md @@ -0,0 +1,457 @@ +# meta.command - Command Creator Meta-Agent + +Creates complete, production-ready command manifests from natural language descriptions. + +## Purpose + +The `meta.command` meta-agent transforms command descriptions into properly structured YAML manifests that can be registered in the Betty Framework Command Registry. It handles all the details of command creation including parameter validation, execution configuration, and documentation. + +## What It Does + +- ✅ Parses natural language command descriptions (Markdown or JSON) +- ✅ Generates complete command manifests in YAML format +- ✅ Validates command structure and execution types +- ✅ Supports all three execution types: agent, skill, workflow +- ✅ Creates proper parameter definitions with type validation +- ✅ Prepares commands for registration via `command.define` skill +- ✅ Supports traceability tracking + +## Usage + +```bash +python3 agents/meta.command/meta_command.py +``` + +### With Traceability + +```bash +python3 agents/meta.command/meta_command.py examples/api_validate_command.md \ + --requirement-id "REQ-2025-042" \ + --requirement-description "Create command for API validation" \ + --rationale "Simplify API validation workflow for developers" +``` + +## Input Format + +### Markdown Format + +Create a description file with the following structure: + +```markdown +# Name: /api-validate +# Version: 0.1.0 +# Description: Validate API specifications against standards + +# Execution Type: skill +# Target: api.validate + +# Parameters: +- spec_file: string (required) - Path to API specification file +- format: enum (optional, default=openapi, values=[openapi,asyncapi,grpc]) - API specification format +- strict: boolean (optional, default=true) - Enable strict validation mode + +# Execution Context: +- format: json +- timeout: 300 + +# Status: active + +# Tags: api, validation, quality +``` + +### JSON Format + +Alternatively, use JSON: + +```json +{ + "name": "/api-validate", + "version": "0.1.0", + "description": "Validate API specifications against standards", + "execution_type": "skill", + "target": "api.validate", + "parameters": [ + { + "name": "spec_file", + "type": "string", + "required": true, + "description": "Path to API specification file" + }, + { + "name": "format", + "type": "enum", + "values": ["openapi", "asyncapi", "grpc"], + "default": "openapi", + "description": "API specification format" + } + ], + "execution_context": { + "format": "json", + "timeout": 300 + }, + "status": "active", + "tags": ["api", "validation", "quality"] +} +``` + +## Command Execution Types + +### 1. Agent Execution + +Use for complex, context-aware tasks requiring reasoning: + +```markdown +# Name: /api-design +# Execution Type: agent +# Target: api.architect +# Description: Design a complete API architecture +``` + +**When to use:** +- Tasks requiring multi-step reasoning +- Context-aware decision making +- Complex analysis or design work + +### 2. Skill Execution + +Use for atomic, deterministic operations: + +```markdown +# Name: /api-validate +# Execution Type: skill +# Target: api.validate +# Description: Validate API specifications +``` + +**When to use:** +- Direct, predictable operations +- Fast, single-purpose tasks +- Composable building blocks + +### 3. Workflow Execution + +Use for orchestrated multi-step processes: + +```markdown +# Name: /api-pipeline +# Execution Type: workflow +# Target: workflows/api-pipeline.yaml +# Description: Execute full API development pipeline +``` + +**When to use:** +- Multi-agent/skill coordination +- Sequential or parallel task execution +- Complex business processes + +## Parameter Types + +### Supported Types + +| Type | Description | Example | +|------|-------------|---------| +| `string` | Text values | `"api-spec.yaml"` | +| `integer` | Whole numbers | `42` | +| `boolean` | true/false | `true` | +| `enum` | Fixed set of values | `["openapi", "asyncapi"]` | +| `array` | Lists of values | `["tag1", "tag2"]` | +| `object` | Structured data | `{"key": "value"}` | + +### Parameter Options + +- `required: true/false` - Whether parameter is mandatory +- `default: value` - Default value if not provided +- `values: [...]` - Allowed values (for enum type) +- `description: "..."` - What the parameter does + +## Examples + +### Example 1: Simple Validation Command + +**Input:** `examples/api-validate-cmd.md` + +```markdown +# Name: /api-validate +# Description: Validate API specification files +# Execution Type: skill +# Target: api.validate + +# Parameters: +- spec_file: string (required) - Path to specification file +- format: enum (optional, default=openapi, values=[openapi,asyncapi]) - Spec format + +# Status: active +# Tags: api, validation +``` + +**Output:** `commands/api-validate.yaml` + +```yaml +name: /api-validate +version: 0.1.0 +description: Validate API specification files +parameters: + - name: spec_file + type: string + required: true + description: Path to specification file + - name: format + type: enum + values: + - openapi + - asyncapi + default: openapi + description: Spec format +execution: + type: skill + target: api.validate +status: active +tags: + - api + - validation +``` + +### Example 2: Agent-Based Design Command + +**Input:** `examples/api-design-cmd.md` + +```markdown +# Name: /api-design +# Description: Design a complete API architecture +# Execution Type: agent +# Target: api.architect + +# Parameters: +- requirements: string (required) - Path to requirements document +- style: enum (optional, default=rest, values=[rest,graphql,grpc]) - API style + +# Execution Context: +- reasoning_mode: iterative +- max_iterations: 10 + +# Status: active +# Tags: api, design, architecture +``` + +**Output:** `commands/api-design.yaml` + +```yaml +name: /api-design +version: 0.1.0 +description: Design a complete API architecture +parameters: + - name: requirements + type: string + required: true + description: Path to requirements document + - name: style + type: enum + values: + - rest + - graphql + - grpc + default: rest + description: API style +execution: + type: agent + target: api.architect + context: + reasoning_mode: iterative + max_iterations: 10 +status: active +tags: + - api + - design + - architecture +``` + +### Example 3: Workflow Command + +**Input:** `examples/deploy-cmd.md` + +```markdown +# Name: /deploy +# Description: Deploy application to specified environment +# Execution Type: workflow +# Target: workflows/deploy-pipeline.yaml + +# Parameters: +- environment: enum (required, values=[dev,staging,production]) - Target environment +- version: string (required) - Version to deploy +- skip_tests: boolean (optional, default=false) - Skip test execution + +# Status: draft +# Tags: deployment, devops +``` + +## Output + +The meta-agent creates: + +1. **Command Manifest** - Complete YAML file in `commands/` directory +2. **Console Output** - Summary of created command +3. **Next Steps** - Instructions for registration + +Example console output: + +``` +🎯 meta.command - Creating command from examples/api-validate-cmd.md + +✨ Command '/api-validate' created successfully! + +📄 Created file: + - commands/api-validate.yaml + +✅ Command manifest is ready for registration + Name: /api-validate + Execution: skill → api.validate + Status: active + +📝 Next steps: + 1. Review the manifest: cat commands/api-validate.yaml + 2. Register command: python3 skills/command.define/command_define.py commands/api-validate.yaml + 3. Verify in registry: cat registry/commands.json +``` + +## Integration with command.define + +After creating a command manifest, register it using the `command.define` skill: + +```bash +# Register the command +python3 skills/command.define/command_define.py commands/api-validate.yaml + +# Verify registration +cat registry/commands.json +``` + +The `command.define` skill will: +- Validate the manifest structure +- Check that the execution target exists +- Add the command to the Command Registry +- Make the command available for use + +## Artifact Flow + +``` +┌──────────────────────────┐ +│ Command Description │ +│ (Markdown or JSON) │ +└──────────┬───────────────┘ + │ consumes + ▼ + ┌──────────────┐ + │ meta.command │ + └──────┬───────┘ + │ produces + ▼ +┌──────────────────────────┐ +│ Command Manifest (YAML) │ +│ commands/*.yaml │ +└──────────┬───────────────┘ + │ + ▼ + ┌──────────────┐ + │command.define│ + │ (skill) │ + └──────┬───────┘ + │ + ▼ +┌──────────────────────────┐ +│ Commands Registry │ +│ registry/commands.json │ +└──────────────────────────┘ +``` + +## Command Naming Conventions + +- ✅ Must start with `/` (e.g., `/api-validate`) +- ✅ Use kebab-case for multi-word commands (e.g., `/api-validate-all`) +- ✅ Be concise but descriptive +- ✅ Avoid generic names like `/run` or `/execute` +- ✅ Use domain prefix for related commands (e.g., `/api-*`, `/db-*`) + +## Validation + +The meta-agent validates: + +- ✅ Required fields present (name, description, execution_type, target) +- ✅ Valid execution type (agent, skill, workflow) +- ✅ Command name starts with `/` +- ✅ Parameter types are valid +- ✅ Enum parameters have values defined +- ✅ Version follows semantic versioning +- ✅ Status is valid (draft, active, deprecated, archived) + +## Error Handling + +Common errors and solutions: + +**Missing required fields:** +``` +❌ Error: Missing required fields: execution_type, target +``` +→ Add all required fields to your description + +**Invalid execution type:** +``` +❌ Error: Invalid execution type: service. Must be one of: agent, skill, workflow +``` +→ Use only valid execution types + +**Invalid parameter type:** +``` +❌ Error: Invalid parameter type: float +``` +→ Use supported parameter types + +## Best Practices + +1. **Clear Descriptions** - Write concise, actionable command descriptions +2. **Proper Parameters** - Define all parameters with types and validation +3. **Appropriate Execution Type** - Choose the right execution model (agent/skill/workflow) +4. **Meaningful Tags** - Add relevant tags for discoverability +5. **Version Management** - Start with 0.1.0, increment appropriately +6. **Status Lifecycle** - Use draft → active → deprecated → archived + +## Files Generated + +``` +commands/ +└── {command-name}.yaml # Command manifest +``` + +## Integration with Meta-Agents + +The `meta.command` agent works alongside: + +- **meta.skill** - Create skills that commands can execute +- **meta.agent** - Create agents that commands can delegate to +- **meta.artifact** - Define artifact types for command I/O +- **meta.compatibility** - Find compatible agents for command workflows + +## Traceability + +Track command creation with requirement metadata: + +```bash +python3 agents/meta.command/meta_command.py examples/api-validate-cmd.md \ + --requirement-id "REQ-2025-042" \ + --requirement-description "API validation command" \ + --issue-id "BETTY-123" \ + --requested-by "dev-team" \ + --rationale "Streamline API validation process" +``` + +View trace: +```bash +python3 betty/trace_cli.py show command.api_validate +``` + +## See Also + +- **command.define skill** - Register command manifests +- **meta.skill** - Create skills for command execution +- **meta.agent** - Create agents for command delegation +- **Command Registry** - `registry/commands.json` +- **Command Infrastructure** - `docs/COMMAND_HOOK_INFRASTRUCTURE.md` diff --git a/agents/meta.command/agent.yaml b/agents/meta.command/agent.yaml new file mode 100644 index 0000000..6ac875e --- /dev/null +++ b/agents/meta.command/agent.yaml @@ -0,0 +1,211 @@ +name: meta.command +version: 0.1.0 +description: | + Creates complete command manifests from natural language descriptions. + + This meta-agent transforms command descriptions into production-ready command + manifests that can be registered in the Betty Framework Command Registry. + + Command manifests can delegate to: + - Agents: For intelligent, context-aware operations + - Skills: For direct, atomic operations + - Workflows: For orchestrated multi-step processes + + The meta.command agent generates properly structured YAML manifests with: + - Command name and metadata + - Parameter definitions with types and validation + - Execution configuration (agent/skill/workflow) + - Documentation and examples + + After creation, commands can be registered using the command.define skill. + +artifact_metadata: + consumes: + - type: command-description + file_pattern: "**/command_description.md" + content_type: "text/markdown" + description: "Natural language description of command requirements" + schema: "schemas/command-description.json" + + produces: + - type: command-manifest + file_pattern: "commands/*.yaml" + content_type: "application/yaml" + description: "Complete command manifest ready for registration" + schema: "schemas/command-manifest.json" + + - type: command-documentation + file_pattern: "commands/*/README.md" + content_type: "text/markdown" + description: "Command documentation with usage examples" + +status: draft +reasoning_mode: iterative +capabilities: + - Transform natural language specifications into validated command manifests + - Recommend appropriate execution targets across agents, skills, and workflows + - Produce documentation and registration-ready assets for new commands +skills_available: + - command.define # Register command in registry + - artifact.define # Generate artifact metadata + +permissions: + - filesystem:read + - filesystem:write + +system_prompt: | + You are meta.command, the command creator for Betty Framework. + + Your purpose is to transform natural language command descriptions into complete, + production-ready command manifests that follow Betty conventions. + + ## Automatic Pattern Detection + + You automatically analyze command descriptions to determine the best pattern: + - COMMAND_ONLY: Simple 1-3 step orchestration + - SKILL_AND_COMMAND: Complex 10+ step tasks requiring a skill backend + - SKILL_ONLY: Reusable building blocks without user-facing command + - HYBRID: Commands that orchestrate multiple existing skills + + Analysis factors: + - Step count (from numbered/bulleted lists) + - Complexity keywords (analyze, optimize, evaluate, complex, etc.) + - Autonomy requirements (intelligent, adaptive, sophisticated, etc.) + - Reusability indicators (composable, shared, library, etc.) + + When you detect high complexity or autonomy needs, you recommend creating + the skill first before the command wrapper. + + ## Your Workflow + + 1. **Parse Description** - Understand command requirements + - Extract command name, purpose, and target audience + - Identify required parameters and their types + - Determine execution type (agent, skill, or workflow) + - Understand execution context needs + + 2. **Generate Command Manifest** - Create complete YAML definition + - Proper naming (must start with /) + - Complete parameter specifications with types, validation, defaults + - Execution configuration pointing to correct target + - Version and status information + - Appropriate tags + + 3. **Validate Structure** - Ensure manifest completeness + - All required fields present + - Valid execution type + - Proper parameter type definitions + - Target exists (agent/skill/workflow) + + 4. **Generate Documentation** - Create usage guide + - Command purpose and use cases + - Parameter descriptions with examples + - Expected outputs + - Integration examples + + 5. **Ready for Registration** - Prepare for command.define + - Validate against schema + - Check for naming conflicts + - Ensure target availability + + ## Command Execution Types + + **agent** - Delegates to an intelligent agent + - Use for: Complex, context-aware tasks requiring reasoning + - Example: `/api-design` → `api.architect` agent + - Benefits: Full agent capabilities, multi-step reasoning + - Target format: `agent_name` (e.g., "api.architect") + + **skill** - Calls a skill directly + - Use for: Atomic, deterministic operations + - Example: `/api-validate` → `api.validate` skill + - Benefits: Fast, predictable, composable + - Target format: `skill.name` (e.g., "api.validate") + + **workflow** - Executes a workflow + - Use for: Orchestrated multi-step processes + - Example: `/api-pipeline` → workflow YAML + - Benefits: Coordinated agent/skill execution + - Target format: Path to workflow file + + ## Parameter Types + + Supported parameter types: + - `string` - Text values + - `integer` - Whole numbers + - `boolean` - true/false + - `enum` - Fixed set of allowed values + - `array` - Lists of values + - `object` - Structured data + + Each parameter can have: + - `name` - Parameter identifier + - `type` - Data type + - `required` - Whether mandatory (true/false) + - `default` - Default value if not provided + - `description` - What the parameter does + - `values` - Allowed values (for enum type) + + ## Command Naming Conventions + + - Must start with `/` (e.g., `/api-validate`) + - Use kebab-case for multi-word commands + - Should be concise but descriptive + - Avoid generic names like `/run` or `/execute` + + ## Command Status + + - `draft` - Under development, not ready for production + - `active` - Production-ready and available + - `deprecated` - Still works but discouraged + - `archived` - No longer available + + ## Structure Example + + ```yaml + name: /api-validate + version: 0.1.0 + description: "Validate API specifications against standards" + + parameters: + - name: spec_file + type: string + required: true + description: "Path to API specification file" + + - name: format + type: enum + values: [openapi, asyncapi, grpc] + default: openapi + description: "API specification format" + + execution: + type: skill + target: api.validate + context: + format: json + + status: active + + tags: [api, validation, quality] + ``` + + ## Quality Standards + + - ✅ Follows Betty command conventions + - ✅ Proper parameter definitions with validation + - ✅ Correct execution type and target + - ✅ Clear, actionable descriptions + - ✅ Appropriate status and tags + - ✅ Ready for command.define registration + + ## Integration with command.define + + After generating the command manifest, users should: + 1. Review the generated YAML file + 2. Test the command locally + 3. Register using: `python3 skills/command.define/command_define.py ` + 4. Verify registration in `registry/commands.json` + + Remember: You're creating user-facing commands that make Betty's capabilities + accessible. Make commands intuitive, well-documented, and easy to use. diff --git a/agents/meta.command/meta_command.py b/agents/meta.command/meta_command.py new file mode 100755 index 0000000..5b7b5c8 --- /dev/null +++ b/agents/meta.command/meta_command.py @@ -0,0 +1,761 @@ +#!/usr/bin/env python3 +""" +meta.command - Command Creator Meta-Agent + +Generates command manifests from natural language descriptions. + +Usage: + python3 agents/meta.command/meta_command.py + +Examples: + python3 agents/meta.command/meta_command.py examples/api_validate_command.md + python3 agents/meta.command/meta_command.py examples/deploy_command.json +""" + +import os +import sys +import json +import yaml +import re +from pathlib import Path +from typing import Dict, List, Any, Optional + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from betty.config import ( + BASE_DIR, + COMMANDS_REGISTRY_FILE, +) +from betty.enums import CommandExecutionType, CommandStatus +from betty.logging_utils import setup_logger +from betty.traceability import get_tracer, RequirementInfo + +logger = setup_logger(__name__) + +# Import artifact validation from artifact.define skill +try: + import importlib.util + artifact_define_path = Path(__file__).parent.parent.parent / "skills" / "artifact.define" / "artifact_define.py" + spec = importlib.util.spec_from_file_location("artifact_define", artifact_define_path) + artifact_define_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(artifact_define_module) + + validate_artifact_type = artifact_define_module.validate_artifact_type + KNOWN_ARTIFACT_TYPES = artifact_define_module.KNOWN_ARTIFACT_TYPES + ARTIFACT_VALIDATION_AVAILABLE = True +except Exception as e: + ARTIFACT_VALIDATION_AVAILABLE = False + + +class CommandCreator: + """Creates command manifests from descriptions""" + + VALID_EXECUTION_TYPES = ["agent", "skill", "workflow"] + VALID_STATUSES = ["draft", "active", "deprecated", "archived"] + VALID_PARAMETER_TYPES = ["string", "integer", "boolean", "enum", "array", "object"] + + # Keywords for complexity analysis + AUTONOMY_KEYWORDS = [ + "analyze", "optimize", "decide", "evaluate", "assess", + "complex", "multi-step", "autonomous", "intelligent", + "adaptive", "sophisticated", "advanced", "comprehensive" + ] + + REUSABILITY_KEYWORDS = [ + "reusable", "composable", "building block", "library", + "utility", "helper", "shared", "common", "core" + ] + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize command creator""" + self.base_dir = Path(base_dir) + self.commands_dir = self.base_dir / "commands" + + def parse_description(self, description_path: str) -> Dict[str, Any]: + """ + Parse command description from Markdown or JSON file + + Args: + description_path: Path to description file + + Returns: + Dict with command configuration + """ + path = Path(description_path) + + if not path.exists(): + raise FileNotFoundError(f"Description file not found: {description_path}") + + # Read file + content = path.read_text() + + # Try JSON first + if path.suffix == ".json": + return json.loads(content) + + # Parse Markdown format + cmd_desc = {} + + # Extract fields using regex patterns + patterns = { + "name": r"#\s*Name:\s*(.+)", + "version": r"#\s*Version:\s*(.+)", + "description": r"#\s*Description:\s*(.+)", + "execution_type": r"#\s*Execution\s*Type:\s*(.+)", + "target": r"#\s*Target:\s*(.+)", + "status": r"#\s*Status:\s*(.+)", + } + + for field, pattern in patterns.items(): + match = re.search(pattern, content, re.IGNORECASE) + if match: + value = match.group(1).strip() + cmd_desc[field] = value + + # Parse parameters section + params_section = re.search( + r"#\s*Parameters:\s*\n(.*?)(?=\n#|\Z)", + content, + re.DOTALL | re.IGNORECASE + ) + + if params_section: + cmd_desc["parameters"] = self._parse_parameters(params_section.group(1)) + + # Parse tags + tags_match = re.search(r"#\s*Tags:\s*(.+)", content, re.IGNORECASE) + if tags_match: + tags_str = tags_match.group(1).strip() + # Parse comma-separated or bracket-enclosed tags + if tags_str.startswith("[") and tags_str.endswith("]"): + tags_str = tags_str[1:-1] + cmd_desc["tags"] = [t.strip() for t in tags_str.split(",")] + + # Parse execution context + context_section = re.search( + r"#\s*Execution\s*Context:\s*\n(.*?)(?=\n#|\Z)", + content, + re.DOTALL | re.IGNORECASE + ) + if context_section: + cmd_desc["execution_context"] = self._parse_context(context_section.group(1)) + + # Parse artifact metadata sections + produces_section = re.search( + r"#\s*Produces\s*Artifacts:\s*\n(.*?)(?=\n#|\Z)", + content, + re.DOTALL | re.IGNORECASE + ) + if produces_section: + cmd_desc["artifact_produces"] = self._parse_artifact_list(produces_section.group(1)) + + consumes_section = re.search( + r"#\s*Consumes\s*Artifacts:\s*\n(.*?)(?=\n#|\Z)", + content, + re.DOTALL | re.IGNORECASE + ) + if consumes_section: + cmd_desc["artifact_consumes"] = self._parse_artifact_list(consumes_section.group(1)) + + # Validate required fields + required = ["name", "description", "execution_type", "target"] + missing = [f for f in required if f not in cmd_desc] + if missing: + raise ValueError(f"Missing required fields: {', '.join(missing)}") + + # Validate execution type + if cmd_desc["execution_type"].lower() not in self.VALID_EXECUTION_TYPES: + raise ValueError( + f"Invalid execution type: {cmd_desc['execution_type']}. " + f"Must be one of: {', '.join(self.VALID_EXECUTION_TYPES)}" + ) + + # Ensure command name starts with / + if not cmd_desc["name"].startswith("/"): + cmd_desc["name"] = "/" + cmd_desc["name"] + + # Set defaults + if "version" not in cmd_desc: + cmd_desc["version"] = "0.1.0" + if "status" not in cmd_desc: + cmd_desc["status"] = "draft" + if "parameters" not in cmd_desc: + cmd_desc["parameters"] = [] + + return cmd_desc + + def _parse_parameters(self, params_text: str) -> List[Dict[str, Any]]: + """Parse parameters from markdown text""" + parameters = [] + + # Match parameter blocks + # Format: - name: type (required/optional) - description + param_pattern = r"-\s+(\w+):\s+(\w+)(?:\s+\(([^)]+)\))?\s+-\s+(.+?)(?=\n-|\n#|\Z)" + matches = re.finditer(param_pattern, params_text, re.DOTALL) + + for match in matches: + name, param_type, modifiers, description = match.groups() + + param = { + "name": name.strip(), + "type": param_type.strip(), + "description": description.strip() + } + + # Parse modifiers (required, optional, default=value) + if modifiers: + modifiers = modifiers.lower() + param["required"] = "required" in modifiers + + # Extract default value + default_match = re.search(r"default[=:]\s*([^,\s]+)", modifiers) + if default_match: + default_val = default_match.group(1) + # Convert types + if param_type == "integer": + default_val = int(default_val) + elif param_type == "boolean": + default_val = default_val.lower() in ("true", "yes", "1") + param["default"] = default_val + + # Extract enum values + values_match = re.search(r"values[=:]\s*\[([^\]]+)\]", modifiers) + if values_match: + param["values"] = [v.strip() for v in values_match.group(1).split(",")] + + parameters.append(param) + + return parameters + + def _parse_context(self, context_text: str) -> Dict[str, Any]: + """Parse execution context from markdown text""" + context = {} + + # Simple key: value parsing + for line in context_text.split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + + match = re.match(r"-\s*(\w+):\s*(.+)", line) + if match: + key, value = match.groups() + # Try to parse as JSON for complex values + try: + context[key] = json.loads(value) + except (json.JSONDecodeError, ValueError): + context[key] = value.strip() + + return context + + def _parse_artifact_list(self, artifact_text: str) -> List[str]: + """Parse artifact list from markdown text""" + artifacts = [] + + for line in artifact_text.split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + + # Match lines starting with - or * + match = re.match(r"[-*]\s*`?([a-z0-9-]+)`?", line) + if match: + artifacts.append(match.group(1)) + + return artifacts + + def analyze_complexity(self, cmd_desc: Dict[str, Any], full_content: str = "") -> Dict[str, Any]: + """ + Analyze command complexity and recommend pattern + + Args: + cmd_desc: Parsed command description + full_content: Full description file content for analysis + + Returns: + Dict with complexity analysis and pattern recommendation + """ + analysis = { + "step_count": 0, + "complexity": "low", + "autonomy_level": "none", + "reusability": "low", + "recommended_pattern": "COMMAND_ONLY", + "should_create_skill": False, + "reasoning": [] + } + + # Count steps from description + # Look for numbered lists, bullet points, or explicit step mentions + step_patterns = [ + r"^\s*\d+\.\s+", # Numbered lists + r"^\s*[-*]\s+", # Bullet points + r"\bstep\s+\d+\b", # Explicit "step N" + ] + + lines = full_content.split("\n") + step_count = 0 + for line in lines: + for pattern in step_patterns: + if re.search(pattern, line, re.IGNORECASE): + step_count += 1 + break + + analysis["step_count"] = step_count + + # Analyze content for keywords + content_lower = full_content.lower() + desc_lower = cmd_desc.get("description", "").lower() + combined = content_lower + " " + desc_lower + + # Check autonomy keywords + autonomy_matches = [kw for kw in self.AUTONOMY_KEYWORDS if kw in combined] + if len(autonomy_matches) >= 3: + analysis["autonomy_level"] = "high" + elif len(autonomy_matches) >= 1: + analysis["autonomy_level"] = "medium" + else: + analysis["autonomy_level"] = "low" + + # Check reusability keywords + reusability_matches = [kw for kw in self.REUSABILITY_KEYWORDS if kw in combined] + if len(reusability_matches) >= 2: + analysis["reusability"] = "high" + elif len(reusability_matches) >= 1: + analysis["reusability"] = "medium" + + # Determine complexity + if step_count >= 10: + analysis["complexity"] = "high" + elif step_count >= 4: + analysis["complexity"] = "medium" + else: + analysis["complexity"] = "low" + + # Estimate lines of logic (rough heuristic) + instruction_lines = sum(1 for line in lines if line.strip() and not line.strip().startswith("#")) + if instruction_lines > 50: + analysis["complexity"] = "high" + + # Decide pattern based on decision tree + if step_count >= 10 or analysis["complexity"] == "high": + analysis["recommended_pattern"] = "SKILL_AND_COMMAND" + analysis["should_create_skill"] = True + analysis["reasoning"].append(f"High complexity: {step_count} steps detected") + + elif analysis["autonomy_level"] == "high": + analysis["recommended_pattern"] = "SKILL_AND_COMMAND" + analysis["should_create_skill"] = True + analysis["reasoning"].append(f"High autonomy: matched keywords {autonomy_matches[:3]}") + + elif analysis["reusability"] == "high": + if step_count <= 3: + analysis["recommended_pattern"] = "SKILL_ONLY" + analysis["should_create_skill"] = True + analysis["reasoning"].append("High reusability but low complexity: create skill only") + else: + analysis["recommended_pattern"] = "SKILL_AND_COMMAND" + analysis["should_create_skill"] = True + analysis["reasoning"].append(f"High reusability with {step_count} steps: create both") + + elif step_count >= 4 and step_count <= 9: + # Medium complexity - could go either way + if analysis["autonomy_level"] == "medium": + analysis["recommended_pattern"] = "SKILL_AND_COMMAND" + analysis["should_create_skill"] = True + analysis["reasoning"].append(f"Medium complexity ({step_count} steps) with some autonomy needs") + else: + analysis["recommended_pattern"] = "COMMAND_ONLY" + analysis["reasoning"].append(f"Medium complexity ({step_count} steps) but simple logic: inline is fine") + + else: + # Low complexity - command only + analysis["recommended_pattern"] = "COMMAND_ONLY" + analysis["reasoning"].append(f"Low complexity ({step_count} steps): inline orchestration is sufficient") + + # Check if execution type already specifies skill + if cmd_desc.get("execution_type") == "skill": + analysis["recommended_pattern"] = "SKILL_AND_COMMAND" + analysis["should_create_skill"] = True + analysis["reasoning"].append("Execution type explicitly set to 'skill'") + + return analysis + + def generate_command_manifest(self, cmd_desc: Dict[str, Any]) -> str: + """ + Generate command manifest YAML + + Args: + cmd_desc: Parsed command description + + Returns: + YAML string + """ + manifest = { + "name": cmd_desc["name"], + "version": cmd_desc["version"], + "description": cmd_desc["description"] + } + + # Add parameters if present + if cmd_desc.get("parameters"): + manifest["parameters"] = cmd_desc["parameters"] + + # Add execution configuration + execution = { + "type": cmd_desc["execution_type"], + "target": cmd_desc["target"] + } + + if cmd_desc.get("execution_context"): + execution["context"] = cmd_desc["execution_context"] + + manifest["execution"] = execution + + # Add status + manifest["status"] = cmd_desc.get("status", "draft") + + # Add tags if present + if cmd_desc.get("tags"): + manifest["tags"] = cmd_desc["tags"] + + # Add artifact metadata if present + if cmd_desc.get("artifact_produces") or cmd_desc.get("artifact_consumes"): + artifact_metadata = {} + + if cmd_desc.get("artifact_produces"): + artifact_metadata["produces"] = [ + {"type": art_type} for art_type in cmd_desc["artifact_produces"] + ] + + if cmd_desc.get("artifact_consumes"): + artifact_metadata["consumes"] = [ + {"type": art_type, "required": True} + for art_type in cmd_desc["artifact_consumes"] + ] + + manifest["artifact_metadata"] = artifact_metadata + + return yaml.dump(manifest, default_flow_style=False, sort_keys=False) + + def validate_artifacts(self, cmd_desc: Dict[str, Any]) -> List[str]: + """ + Validate that artifact types exist in the known registry. + + Args: + cmd_desc: Parsed command description + + Returns: + List of warning messages + """ + warnings = [] + + if not ARTIFACT_VALIDATION_AVAILABLE: + warnings.append( + "Artifact validation skipped: artifact.define skill not available" + ) + return warnings + + # Validate produced artifacts + for artifact_type in cmd_desc.get("artifact_produces", []): + is_valid, warning = validate_artifact_type(artifact_type) + if not is_valid and warning: + warnings.append(f"Produces: {warning}") + + # Validate consumed artifacts + for artifact_type in cmd_desc.get("artifact_consumes", []): + is_valid, warning = validate_artifact_type(artifact_type) + if not is_valid and warning: + warnings.append(f"Consumes: {warning}") + + return warnings + + def validate_target(self, cmd_desc: Dict[str, Any]) -> List[str]: + """ + Validate that the target skill or agent exists. + + Args: + cmd_desc: Parsed command description + + Returns: + List of warning messages + """ + warnings = [] + execution_type = cmd_desc.get("execution_type", "").lower() + target = cmd_desc.get("target", "") + + if execution_type == "skill": + # Check if skill exists in registry or skills directory + skill_registry = self.base_dir / "registry" / "skills.json" + skill_dir = self.base_dir / "skills" / target.replace(".", "/") + + skill_exists = False + if skill_registry.exists(): + try: + with open(skill_registry) as f: + registry = json.load(f) + if target in registry.get("skills", {}): + skill_exists = True + except Exception: + pass + + if not skill_exists and not skill_dir.exists(): + warnings.append( + f"Target skill '{target}' not found in registry or skills directory. " + f"You may need to create it using meta.skill first." + ) + + elif execution_type == "agent": + # Check if agent exists in agents directory + agent_dir = self.base_dir / "agents" / target + if not agent_dir.exists(): + warnings.append( + f"Target agent '{target}' not found in agents directory. " + f"You may need to create it using meta.agent first." + ) + + return warnings + + def create_command( + self, + description_path: str, + requirement: Optional[RequirementInfo] = None + ) -> Dict[str, Any]: + """ + Create command manifest from description file + + Args: + description_path: Path to description file + requirement: Optional requirement information for traceability + + Returns: + Dict with creation results + """ + try: + print(f"🎯 meta.command - Creating command from {description_path}\n") + + # Read full content for analysis + with open(description_path, 'r') as f: + full_content = f.read() + + # Parse description + cmd_desc = self.parse_description(description_path) + + # Validate artifacts + artifact_warnings = self.validate_artifacts(cmd_desc) + if artifact_warnings: + print("\n⚠️ Artifact Validation Warnings:") + for warning in artifact_warnings: + print(f" {warning}") + print() + + # Validate target skill/agent + target_warnings = self.validate_target(cmd_desc) + if target_warnings: + print("\n⚠️ Target Validation Warnings:") + for warning in target_warnings: + print(f" {warning}") + print() + + # Analyze complexity and recommend pattern + analysis = self.analyze_complexity(cmd_desc, full_content) + + # Display analysis + print(f"📊 Complexity Analysis:") + print(f" Steps detected: {analysis['step_count']}") + print(f" Complexity: {analysis['complexity']}") + print(f" Autonomy level: {analysis['autonomy_level']}") + print(f" Reusability: {analysis['reusability']}") + print(f"\n💡 Recommended Pattern: {analysis['recommended_pattern']}") + for reason in analysis['reasoning']: + print(f" • {reason}") + print() + + # Generate manifest YAML + manifest_yaml = self.generate_command_manifest(cmd_desc) + + # Ensure commands directory exists + self.commands_dir.mkdir(parents=True, exist_ok=True) + + # Determine output filename + # Remove leading / and replace spaces/special chars with hyphens + filename = cmd_desc["name"].lstrip("/").replace(" ", "-").lower() + filename = re.sub(r"[^a-z0-9-]", "", filename) + manifest_file = self.commands_dir / f"{filename}.yaml" + + # Write manifest file + manifest_file.write_text(manifest_yaml) + + print(f"✨ Command '{cmd_desc['name']}' created successfully!\n") + print(f"📄 Created file:") + print(f" - {manifest_file}\n") + print(f"✅ Command manifest is ready for registration") + print(f" Name: {cmd_desc['name']}") + print(f" Execution: {cmd_desc['execution_type']} → {cmd_desc['target']}") + print(f" Status: {cmd_desc.get('status', 'draft')}\n") + + # Display skill creation recommendation if needed + if analysis['should_create_skill']: + print(f"⚠️ RECOMMENDATION: Create the skill first!") + print(f" Pattern: {analysis['recommended_pattern']}") + print(f"\n This command delegates to a skill ({cmd_desc['target']}),") + print(f" but that skill may not exist yet.\n") + print(f" Suggested workflow:") + print(f" 1. Create skill: python3 agents/meta.skill/meta_skill.py ") + print(f" - Skill should implement: {cmd_desc['target']}") + print(f" - Include all complex logic from the command description") + print(f" 2. Test skill: python3 skills/{cmd_desc['target'].replace('.', '/')}/{cmd_desc['target'].replace('.', '_')}.py") + print(f" 3. Review this command manifest: cat {manifest_file}") + print(f" 4. Register command: python3 skills/command.define/command_define.py {manifest_file}") + print(f" 5. Verify in registry: cat registry/commands.json") + print(f"\n See docs/SKILL_COMMAND_DECISION_TREE.md for pattern details\n") + else: + print(f"📝 Next steps:") + print(f" 1. Review the manifest: cat {manifest_file}") + print(f" 2. Register command: python3 skills/command.define/command_define.py {manifest_file}") + print(f" 3. Verify in registry: cat registry/commands.json") + + result = { + "ok": True, + "status": "success", + "command_name": cmd_desc["name"], + "manifest_file": str(manifest_file), + "complexity_analysis": analysis, + "artifact_warnings": artifact_warnings, + "target_warnings": target_warnings + } + + # Log traceability if requirement provided + trace_id = None + if requirement: + try: + tracer = get_tracer() + + # Create component ID from command name + component_id = f"command.{filename.replace('-', '_')}" + + trace_id = tracer.log_creation( + component_id=component_id, + component_name=cmd_desc["name"], + component_type="command", + component_version=cmd_desc["version"], + component_file_path=str(manifest_file), + input_source_path=description_path, + created_by_tool="meta.command", + created_by_version="0.1.0", + requirement=requirement, + tags=["command", "auto-generated"] + cmd_desc.get("tags", []), + project="Betty Framework" + ) + + # Log validation check + validation_details = { + "checks_performed": [ + {"name": "command_structure", "status": "passed"}, + {"name": "execution_type_validation", "status": "passed", + "message": f"Valid execution type: {cmd_desc['execution_type']}"}, + {"name": "name_validation", "status": "passed", + "message": f"Command name follows convention: {cmd_desc['name']}"} + ] + } + + # Check parameters + if cmd_desc.get("parameters"): + validation_details["checks_performed"].append({ + "name": "parameters_validation", + "status": "passed", + "message": f"Validated {len(cmd_desc['parameters'])} parameters" + }) + + tracer.log_verification( + component_id=component_id, + check_type="validation", + tool="meta.command", + result="passed", + details=validation_details + ) + + result["trace_id"] = trace_id + result["component_id"] = component_id + + except Exception as e: + print(f"⚠️ Warning: Could not log traceability: {e}") + + return result + + except Exception as e: + print(f"❌ Error creating command: {e}") + logger.error(f"Error creating command: {e}", exc_info=True) + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="meta.command - Create command manifests from descriptions" + ) + parser.add_argument( + "description", + help="Path to command description file (.md or .json)" + ) + + # Traceability arguments + parser.add_argument( + "--requirement-id", + help="Requirement identifier (e.g., REQ-2025-001)" + ) + parser.add_argument( + "--requirement-description", + help="What this command accomplishes" + ) + parser.add_argument( + "--requirement-source", + help="Source document" + ) + parser.add_argument( + "--issue-id", + help="Issue tracking ID (e.g., JIRA-123)" + ) + parser.add_argument( + "--requested-by", + help="Who requested this" + ) + parser.add_argument( + "--rationale", + help="Why this is needed" + ) + + args = parser.parse_args() + + # Create requirement info if provided + requirement = None + if args.requirement_id and args.requirement_description: + requirement = RequirementInfo( + id=args.requirement_id, + description=args.requirement_description, + source=args.requirement_source, + issue_id=args.issue_id, + requested_by=args.requested_by, + rationale=args.rationale + ) + + creator = CommandCreator() + result = creator.create_command(args.description, requirement=requirement) + + # Display traceability info if available + if result.get("trace_id"): + print(f"\n📝 Traceability: {result['trace_id']}") + print(f" View trace: python3 betty/trace_cli.py show {result['component_id']}") + + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/agents/meta.compatibility/README.md b/agents/meta.compatibility/README.md new file mode 100644 index 0000000..bd3ecf3 --- /dev/null +++ b/agents/meta.compatibility/README.md @@ -0,0 +1,469 @@ +# meta.compatibility - Agent Compatibility Analyzer + +Analyzes agent compatibility and discovers multi-agent workflows based on artifact flows. + +## Overview + +**meta.compatibility** helps Claude discover which agents can work together by analyzing what artifacts they produce and consume. It enables intelligent multi-agent orchestration by suggesting compatible combinations and detecting pipeline gaps. + +**What it does:** +- Scans all agents and extracts artifact metadata +- Builds compatibility maps (who produces/consumes what) +- Finds compatible agents based on artifact flows +- Suggests multi-agent pipelines for goals +- Generates complete compatibility graphs +- Detects gaps (consumed but not produced artifacts) + +## Quick Start + +### Find Compatible Agents + +```bash +python3 agents/meta.compatibility/meta_compatibility.py find-compatible meta.agent +``` + +Output: +``` +Agent: meta.agent +Produces: agent-definition, agent-documentation +Consumes: agent-description + +✅ Can feed outputs to (1 agents): + • meta.compatibility (via agent-definition) + +⚠️ Gaps (1): + • agent-description: No agents produce 'agent-description' (required by meta.agent) +``` + +### Suggest Pipeline + +```bash +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "Create and analyze an agent" +``` + +Output: +``` +📋 Pipeline 1: meta.agent Pipeline + Pipeline starting with meta.agent + Steps: + 1. meta.agent - Meta-agent that creates other agents... + 2. meta.compatibility - Analyzes agent and skill compatibility... +``` + +### Analyze Agent + +```bash +python3 agents/meta.compatibility/meta_compatibility.py analyze meta.agent +``` + +### List All Compatibility + +```bash +python3 agents/meta.compatibility/meta_compatibility.py list-all +``` + +Output: +``` +Total Agents: 7 +Total Artifact Types: 16 +Total Relationships: 3 + +⚠️ Global Gaps (5): + • agent-description: Consumed by 1 agents but no producers + ... +``` + +## Commands + +### find-compatible + +Find agents compatible with a specific agent. + +```bash +python3 agents/meta.compatibility/meta_compatibility.py find-compatible AGENT_NAME [--format json|yaml|text] +``` + +**Shows:** +- What the agent produces +- What the agent consumes +- Agents that can consume its outputs +- Agents that can provide its inputs +- Gaps (missing producers) + +### suggest-pipeline + +Suggest multi-agent pipeline for a goal. + +```bash +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "GOAL" [--artifacts TYPE1 TYPE2...] [--format json|yaml|text] +``` + +**Examples:** +```bash +# Suggest pipeline for goal +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "Design and validate APIs" + +# With required artifacts +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "Process data" --artifacts openapi-spec validation-report +``` + +**Shows:** +- Suggested pipelines (ranked) +- Steps in each pipeline +- Artifact flows between agents +- Whether pipeline is complete (no gaps) + +### analyze + +Complete compatibility analysis for one agent. + +```bash +python3 agents/meta.compatibility/meta_compatibility.py analyze AGENT_NAME [--format json|yaml|text] +``` + +**Shows:** +- Full compatibility report +- Compatible agents (upstream/downstream) +- Suggested workflows +- Gaps and warnings + +### list-all + +Generate complete compatibility graph for all agents. + +```bash +python3 agents/meta.compatibility/meta_compatibility.py list-all [--format json|yaml|text] +``` + +**Shows:** +- All agents in the system +- All relationships +- All artifact types +- Global gaps +- Statistics + +## Output Formats + +### Text (default) + +Human-readable output with emojis and formatting. + +### JSON + +Machine-readable JSON for programmatic use. + +```bash +python3 agents/meta.compatibility/meta_compatibility.py find-compatible meta.agent --format json > meta_agent_compatibility.json +``` + +### YAML + +YAML format for configuration or documentation. + +```bash +python3 agents/meta.compatibility/meta_compatibility.py list-all --format yaml > compatibility_graph.yaml +``` + +## How It Works + +### 1. Agent Scanning + +Scans `agents/` directory for all `agent.yaml` files: + +```python +for agent_dir in agents_dir.iterdir(): + agent_yaml = agent_dir / "agent.yaml" + # Load and parse agent definition +``` + +### 2. Artifact Extraction + +Extracts artifact_metadata from each agent: + +```yaml +artifact_metadata: + produces: + - type: openapi-spec + consumes: + - type: api-requirements +``` + +### 3. Compatibility Mapping + +Builds map of artifact types to producers/consumers: + +``` +openapi-spec: + producers: [api.define, api.architect] + consumers: [api.validate, api.code-generator] +``` + +### 4. Relationship Discovery + +For each agent: +- Find agents that can consume its outputs +- Find agents that can provide its inputs +- Detect gaps (missing producers) + +### 5. Pipeline Suggestion + +Uses keyword matching and artifact analysis: +- Match goal keywords to agent names/descriptions +- Build pipeline from artifact flows +- Rank by completeness and length +- Return top suggestions + +## Integration + +### With meta.agent +After creating an agent, analyze its compatibility: + +```bash +# Create agent +python3 agents/meta.agent/meta_agent.py description.md + +# Analyze compatibility +python3 agents/meta.compatibility/meta_compatibility.py analyze new-agent + +# Find who can work with it +python3 agents/meta.compatibility/meta_compatibility.py find-compatible new-agent +``` + +### With meta.suggest + +meta.suggest uses meta.compatibility to make recommendations: + +```bash +python3 agents/meta.suggest/meta_suggest.py --context meta.agent +``` + +Internally calls meta.compatibility to find next steps. + +## Common Workflows + +### Workflow 1: Understand Agent Ecosystem + +```bash +# See all compatibility +python3 agents/meta.compatibility/meta_compatibility.py list-all + +# Analyze each agent +for agent in meta.agent meta.artifact meta.compatibility meta.suggest; do + echo "=== $agent ===" + python3 agents/meta.compatibility/meta_compatibility.py analyze $agent +done +``` + +### Workflow 2: Build Multi-Agent Pipeline + +```bash +# Suggest pipeline +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "Create and test an agent" + +# Get JSON for workflow automation +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "My goal" --format json > pipeline.json +``` + +### Workflow 3: Find Gaps + +```bash +# Find global gaps +python3 agents/meta.compatibility/meta_compatibility.py list-all | grep "Gaps:" + +# Analyze specific agent gaps +python3 agents/meta.compatibility/meta_compatibility.py find-compatible api.architect +``` + +## Artifact Types + +### Consumes + +- **agent-definition** - Agent configurations + - Pattern: `agents/*/agent.yaml` + +- **registry-data** - Skills and agents registry + - Pattern: `registry/*.json` + +### Produces + +- **compatibility-graph** - Agent relationship maps + - Pattern: `*.compatibility.json` + - Schema: `schemas/compatibility-graph.json` + +- **pipeline-suggestion** - Multi-agent workflows + - Pattern: `*.pipeline.json` + - Schema: `schemas/pipeline-suggestion.json` + +## Understanding Output + +### Can Feed To + +Agents that can consume this agent's outputs. + +``` +✅ Can feed outputs to (2 agents): + • api.validator (via openapi-spec) + • api.code-generator (via openapi-spec) +``` + +Means: +- api.architect produces openapi-spec +- Both api.validator and api.code-generator consume openapi-spec +- You can run: api.architect → api.validator +- Or: api.architect → api.code-generator + +### Can Receive From + +Agents that can provide this agent's inputs. + +``` +⬅️ Can receive inputs from (1 agents): + • api.requirements-analyzer (via api-requirements) +``` + +Means: +- api.architect needs api-requirements +- api.requirements-analyzer produces api-requirements +- You can run: api.requirements-analyzer → api.architect + +### Gaps + +Missing artifacts in the ecosystem. + +``` +⚠️ Gaps (1): + • agent-description: No agents produce 'agent-description' +``` + +Means: +- meta.agent needs agent-description input +- No agent produces it (it's user-provided) +- This is expected for user inputs + +### Complete vs Incomplete Pipelines + +**Complete Pipeline:** +``` +Complete: ✅ Yes +``` +All consumed artifacts are produced by pipeline steps. + +**Incomplete Pipeline:** +``` +Complete: ❌ No +Gaps: agent-description, registry-data +``` +Some consumed artifacts aren't produced. Requires user input or additional agents. + +## Tips & Best Practices + +### Finding Compatible Agents + +Use specific artifact types: +```bash +# Instead of generic goal +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "Process stuff" + +# Use specific artifacts +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "Validate API" --artifacts openapi-spec +``` + +### Understanding Gaps + +Not all gaps are problems: +- **User inputs** (agent-description, api-requirements) - Expected +- **Missing producers** for internal artifacts - Need new agents/skills + +### Building Pipelines + +Start with compatibility analysis: +1. Understand what each agent needs/produces +2. Find compatible combinations +3. Build pipeline step-by-step +4. Validate no gaps exist (or gaps are user inputs) + +## Troubleshooting + +### Agent not found + +``` +Error: Agent 'my-agent' not found +``` + +**Solutions:** +- Check agent exists in `agents/` directory +- Ensure `agent.yaml` exists +- Verify agent name in agent.yaml matches + +### No compatible agents found + +``` +Can feed outputs to (0 agents) +Can receive inputs from (0 agents) +``` + +**Causes:** +- Agent is isolated (no shared artifact types) +- Agent uses custom artifact types +- No other agents exist yet + +**Solutions:** +- Create agents with compatible artifact types +- Use standard artifact types +- Check artifact_metadata is properly defined + +### Empty pipeline suggestions + +``` +Error: Could not determine relevant agents for goal +``` + +**Solutions:** +- Be more specific in goal description +- Mention artifact types explicitly +- Use `--artifacts` flag + +## Architecture + +``` +meta.compatibility + ├─ Scans: agents/ directory + ├─ Analyzes: artifact_metadata + ├─ Builds: compatibility maps + ├─ Produces: compatibility graphs + └─ Used by: meta.suggest, Claude +``` + +## Examples + +See test runs: +```bash +# Example 1: Find compatible agents +python3 agents/meta.compatibility/meta_compatibility.py find-compatible meta.agent + +# Example 2: Suggest pipeline +python3 agents/meta.compatibility/meta_compatibility.py suggest-pipeline "Create agent and check compatibility" + +# Example 3: Full analysis +python3 agents/meta.compatibility/meta_compatibility.py analyze api.architect + +# Example 4: Export to JSON +python3 agents/meta.compatibility/meta_compatibility.py list-all --format json > graph.json +``` + +## Related Documentation + +- [META_AGENTS.md](../../docs/META_AGENTS.md) - Meta-agent ecosystem +- [ARTIFACT_STANDARDS.md](../../docs/ARTIFACT_STANDARDS.md) - Artifact system +- [compatibility-graph schema](../../schemas/compatibility-graph.json) +- [pipeline-suggestion schema](../../schemas/pipeline-suggestion.json) + +## How Claude Uses This + +Claude can: +1. **Discover capabilities** - "What agents can work with openapi-spec?" +2. **Build workflows** - "How do I design and validate an API?" +3. **Make decisions** - "What should I run next?" +4. **Detect gaps** - "What's missing from the ecosystem?" + +meta.compatibility enables autonomous multi-agent orchestration! diff --git a/agents/meta.compatibility/agent.yaml b/agents/meta.compatibility/agent.yaml new file mode 100644 index 0000000..e725a7a --- /dev/null +++ b/agents/meta.compatibility/agent.yaml @@ -0,0 +1,130 @@ +name: meta.compatibility +version: 0.1.0 +description: | + Analyzes agent and skill compatibility to discover multi-agent workflows. + + This meta-agent helps Claude discover which agents can work together by + analyzing artifact flows - what agents produce and what others consume. + + Enables intelligent orchestration by suggesting compatible agent combinations + and detecting potential pipeline gaps. + +artifact_metadata: + consumes: + - type: agent-definition + file_pattern: "agents/*/agent.yaml" + description: "Agent definitions to analyze for compatibility" + + - type: registry-data + file_pattern: "registry/*.json" + description: "Skills and agents registry" + + produces: + - type: compatibility-graph + file_pattern: "*.compatibility.json" + content_type: "application/json" + schema: "schemas/compatibility-graph.json" + description: "Agent relationship graph showing artifact flows" + + - type: pipeline-suggestion + file_pattern: "*.pipeline.json" + content_type: "application/json" + schema: "schemas/pipeline-suggestion.json" + description: "Suggested multi-agent workflows" + +status: draft +reasoning_mode: iterative +capabilities: + - Build compatibility graphs that connect agent inputs and outputs + - Recommend orchestrated workflows that minimize gaps and conflicts + - Surface registry insights to guide creation of missing capabilities +skills_available: + - agent.compose # Analyze artifact flows + - artifact.define # Understand artifact types + +permissions: + - filesystem:read + +system_prompt: | + You are meta.compatibility, the agent compatibility analyzer. + + Your purpose is to help Claude discover which agents work together by + analyzing what artifacts they produce and consume. + + ## Your Responsibilities + + 1. **Analyze Compatibility** + - Scan all agent definitions + - Extract artifact metadata (produces/consumes) + - Find matching artifact types + - Identify compatible agent pairs + + 2. **Suggest Pipelines** + - Recommend multi-agent workflows + - Ensure artifact flow is complete (no gaps) + - Prioritize common use cases + - Provide clear rationale + + 3. **Detect Gaps** + - Find consumed artifacts that aren't produced + - Identify missing agents in pipelines + - Suggest what needs to be created + + 4. **Generate Compatibility Graphs** + - Visual representation of agent relationships + - Show artifact flows between agents + - Highlight compatible combinations + + ## Commands You Support + + **Find Compatible Agents:** + ```bash + /meta/compatibility find-compatible api.architect + ``` + Returns agents that can consume api.architect's outputs. + + **Suggest Pipeline:** + ```bash + /meta/compatibility suggest-pipeline "Design and implement an API" + ``` + Returns multi-agent workflow for the task. + + **Analyze Agent:** + ```bash + /meta/compatibility analyze api.architect + ``` + Returns full compatibility analysis for one agent. + + **List All Compatibility:** + ```bash + /meta/compatibility list-all + ``` + Returns complete compatibility graph for all agents. + + ## Analysis Criteria + + Two agents are compatible if: + - Agent A produces artifact type X + - Agent B consumes artifact type X + - The artifact schemas are compatible + + ## Pipeline Suggestion Criteria + + A good pipeline: + - Has no gaps (all consumed artifacts are produced) + - Follows logical workflow order + - Matches the user's stated goal + - Uses minimal agents (efficiency) + - Includes validation steps when appropriate + + ## Output Format + + Always provide: + - **Compatible agents**: List with rationale + - **Artifact flows**: What flows between agents + - **Suggested pipelines**: Step-by-step workflows + - **Gaps**: Any missing artifacts or agents + - **Confidence**: How confident you are in the suggestions + + Remember: You enable intelligent orchestration by making compatibility + discoverable. Help Claude make smart choices about which agents to use together. diff --git a/agents/meta.compatibility/meta_compatibility.py b/agents/meta.compatibility/meta_compatibility.py new file mode 100755 index 0000000..a43c015 --- /dev/null +++ b/agents/meta.compatibility/meta_compatibility.py @@ -0,0 +1,698 @@ +#!/usr/bin/env python3 +""" +meta.compatibility - Agent Compatibility Analyzer + +Analyzes agent and skill compatibility to discover multi-agent workflows. +Helps Claude orchestrate by showing which agents can work together. +""" + +import json +import yaml +import sys +import os +from pathlib import Path +from typing import Dict, List, Any, Optional, Set, Tuple +from collections import defaultdict + +# Add parent directory to path for imports +parent_dir = str(Path(__file__).parent.parent.parent) +sys.path.insert(0, parent_dir) + +from betty.provenance import compute_hash, get_provenance_logger +from betty.config import REGISTRY_FILE, REGISTRY_DIR + + +class CompatibilityAnalyzer: + """Analyzes agent compatibility based on artifact flows""" + + def __init__(self, base_dir: str = "."): + """Initialize with base directory""" + self.base_dir = Path(base_dir) + self.agents_dir = self.base_dir / "agents" + self.agents = {} # name -> agent definition + self.compatibility_map = {} # artifact_type -> {producers: [], consumers: []} + + def scan_agents(self) -> Dict[str, Any]: + """ + Scan agents directory and load all agent definitions + + Returns: + Dictionary of agent_name -> agent_definition + """ + self.agents = {} + + if not self.agents_dir.exists(): + return self.agents + + for agent_dir in self.agents_dir.iterdir(): + if agent_dir.is_dir(): + agent_yaml = agent_dir / "agent.yaml" + if agent_yaml.exists(): + with open(agent_yaml) as f: + agent_def = yaml.safe_load(f) + if agent_def and "name" in agent_def: + self.agents[agent_def["name"]] = agent_def + + return self.agents + + def extract_artifacts(self, agent_def: Dict[str, Any]) -> Tuple[Set[str], Set[str]]: + """ + Extract artifact types from agent definition + + Args: + agent_def: Agent definition dictionary + + Returns: + Tuple of (produces_set, consumes_set) + """ + produces = set() + consumes = set() + + artifact_metadata = agent_def.get("artifact_metadata", {}) + + # Extract produced artifacts + for artifact in artifact_metadata.get("produces", []): + if isinstance(artifact, dict) and "type" in artifact: + produces.add(artifact["type"]) + elif isinstance(artifact, str): + produces.add(artifact) + + # Extract consumed artifacts + for artifact in artifact_metadata.get("consumes", []): + if isinstance(artifact, dict) and "type" in artifact: + consumes.add(artifact["type"]) + elif isinstance(artifact, str): + consumes.add(artifact) + + return produces, consumes + + def build_compatibility_map(self) -> Dict[str, Dict[str, List[str]]]: + """ + Build map of artifact types to producers/consumers + + Returns: + Dictionary mapping artifact_type -> {producers: [], consumers: []} + """ + self.compatibility_map = defaultdict(lambda: {"producers": [], "consumers": []}) + + for agent_name, agent_def in self.agents.items(): + produces, consumes = self.extract_artifacts(agent_def) + + for artifact_type in produces: + self.compatibility_map[artifact_type]["producers"].append(agent_name) + + for artifact_type in consumes: + self.compatibility_map[artifact_type]["consumers"].append(agent_name) + + return dict(self.compatibility_map) + + def find_compatible(self, agent_name: str) -> Dict[str, Any]: + """ + Find agents compatible with the specified agent + + Args: + agent_name: Name of agent to analyze + + Returns: + Dictionary with compatible agents and rationale + """ + if agent_name not in self.agents: + return { + "error": f"Agent '{agent_name}' not found", + "available_agents": list(self.agents.keys()) + } + + agent_def = self.agents[agent_name] + produces, consumes = self.extract_artifacts(agent_def) + + result = { + "agent": agent_name, + "produces": list(produces), + "consumes": list(consumes), + "can_feed_to": [], # Agents that can consume this agent's outputs + "can_receive_from": [], # Agents that can provide this agent's inputs + "gaps": [] # Missing artifacts + } + + # Find agents that can consume this agent's outputs + for artifact_type in produces: + consumers = self.compatibility_map.get(artifact_type, {}).get("consumers", []) + for consumer in consumers: + if consumer != agent_name: + result["can_feed_to"].append({ + "agent": consumer, + "artifact": artifact_type, + "rationale": f"{agent_name} produces '{artifact_type}' which {consumer} consumes" + }) + + # Find agents that can provide this agent's inputs + for artifact_type in consumes: + producers = self.compatibility_map.get(artifact_type, {}).get("producers", []) + if not producers: + result["gaps"].append({ + "artifact": artifact_type, + "issue": f"No agents produce '{artifact_type}' (required by {agent_name})", + "severity": "high" + }) + else: + for producer in producers: + if producer != agent_name: + result["can_receive_from"].append({ + "agent": producer, + "artifact": artifact_type, + "rationale": f"{producer} produces '{artifact_type}' which {agent_name} needs" + }) + + return result + + def suggest_pipeline(self, goal: str, required_artifacts: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Suggest multi-agent pipeline for a goal + + Args: + goal: Natural language description of what to accomplish + required_artifacts: Optional list of artifact types needed + + Returns: + Suggested pipeline with steps and rationale + """ + # Simple keyword matching for now (can be enhanced with ML later) + goal_lower = goal.lower() + + keywords_to_agents = { + "api": ["api.architect", "meta.agent"], + "design api": ["api.architect"], + "validate": ["api.architect"], + "create agent": ["meta.agent"], + "agent": ["meta.agent"], + "artifact": ["meta.artifact"], + "optimize": [], # No optimizer yet, but we have the artifact type + } + + # Find relevant agents + relevant_agents = set() + for keyword, agents in keywords_to_agents.items(): + if keyword in goal_lower: + relevant_agents.update([a for a in agents if a in self.agents]) + + if not relevant_agents and required_artifacts: + # Find agents that produce the required artifacts + for artifact_type in required_artifacts: + producers = self.compatibility_map.get(artifact_type, {}).get("producers", []) + relevant_agents.update(producers) + + if not relevant_agents: + return { + "error": "Could not determine relevant agents for goal", + "suggestion": "Try being more specific or mention required artifact types", + "goal": goal + } + + # Build pipeline by analyzing artifact flows + pipelines = [] + + for start_agent in relevant_agents: + pipeline = self._build_pipeline_from_agent(start_agent, goal) + if pipeline: + pipelines.append(pipeline) + + # Rank pipelines by completeness and length + pipelines.sort(key=lambda p: ( + -len([s for s in p.get("steps", [])]), # Prefer shorter pipelines + -p.get("confidence_score", 0) # Higher confidence + )) + + if not pipelines: + return { + "error": "Could not build complete pipeline", + "relevant_agents": list(relevant_agents), + "goal": goal + } + + return { + "goal": goal, + "pipelines": pipelines[:3], # Top 3 suggestions + "confidence": "medium" if len(pipelines) > 1 else "low" + } + + def _build_pipeline_from_agent(self, start_agent: str, goal: str) -> Optional[Dict[str, Any]]: + """ + Build a pipeline starting from a specific agent + + Args: + start_agent: Agent to start pipeline from + goal: Goal description + + Returns: + Pipeline dictionary or None + """ + if start_agent not in self.agents: + return None + + agent_def = self.agents[start_agent] + produces, consumes = self.extract_artifacts(agent_def) + + pipeline = { + "name": f"{start_agent.title()} Pipeline", + "description": f"Pipeline starting with {start_agent}", + "steps": [ + { + "step": 1, + "agent": start_agent, + "description": agent_def.get("description", "").split("\n")[0], + "produces": list(produces), + "consumes": list(consumes) + } + ], + "artifact_flow": [], + "confidence_score": 0.5 + } + + # Try to add compatible next steps + compatibility = self.find_compatible(start_agent) + + for compatible in compatibility.get("can_feed_to", [])[:2]: # Max 2 next steps + next_agent = compatible["agent"] + if next_agent in self.agents: + next_def = self.agents[next_agent] + next_produces, next_consumes = self.extract_artifacts(next_def) + + pipeline["steps"].append({ + "step": len(pipeline["steps"]) + 1, + "agent": next_agent, + "description": next_def.get("description", "").split("\n")[0], + "produces": list(next_produces), + "consumes": list(next_consumes) + }) + + pipeline["artifact_flow"].append({ + "from": start_agent, + "to": next_agent, + "artifact": compatible["artifact"] + }) + + pipeline["confidence_score"] += 0.2 + + # Calculate if pipeline has gaps + all_produces = set() + all_consumes = set() + for step in pipeline["steps"]: + all_produces.update(step.get("produces", [])) + all_consumes.update(step.get("consumes", [])) + + gaps = all_consumes - all_produces + if not gaps: + pipeline["confidence_score"] += 0.3 + pipeline["complete"] = True + else: + pipeline["complete"] = False + pipeline["gaps"] = list(gaps) + + return pipeline + + def generate_compatibility_graph(self) -> Dict[str, Any]: + """ + Generate complete compatibility graph for all agents + + Returns: + Compatibility graph structure + """ + graph = { + "agents": [], + "relationships": [], + "artifact_types": [], + "gaps": [], + "metadata": { + "total_agents": len(self.agents), + "total_artifact_types": len(self.compatibility_map) + } + } + + # Add agents + for agent_name, agent_def in self.agents.items(): + produces, consumes = self.extract_artifacts(agent_def) + + graph["agents"].append({ + "name": agent_name, + "description": agent_def.get("description", "").split("\n")[0], + "produces": list(produces), + "consumes": list(consumes) + }) + + # Add relationships + for agent_name in self.agents: + compatibility = self.find_compatible(agent_name) + + for compatible in compatibility.get("can_feed_to", []): + graph["relationships"].append({ + "from": agent_name, + "to": compatible["agent"], + "artifact": compatible["artifact"], + "type": "produces_for" + }) + + # Add artifact types + for artifact_type, info in self.compatibility_map.items(): + graph["artifact_types"].append({ + "type": artifact_type, + "producers": info["producers"], + "consumers": info["consumers"], + "producer_count": len(info["producers"]), + "consumer_count": len(info["consumers"]) + }) + + # Find global gaps + for artifact_type, info in self.compatibility_map.items(): + if not info["producers"] and info["consumers"]: + graph["gaps"].append({ + "artifact": artifact_type, + "issue": f"Consumed by {len(info['consumers'])} agents but no producers", + "consumers": info["consumers"], + "severity": "high" + }) + + return graph + + def analyze_agent(self, agent_name: str) -> Dict[str, Any]: + """ + Complete compatibility analysis for one agent + + Args: + agent_name: Name of agent to analyze + + Returns: + Comprehensive analysis + """ + compatibility = self.find_compatible(agent_name) + + if "error" in compatibility: + return compatibility + + # Add suggested workflows + workflows = [] + + # Workflow 1: As a starting point + if compatibility["can_feed_to"]: + workflow = { + "name": f"Start with {agent_name}", + "description": f"Use {agent_name} as the first step", + "agents": [agent_name] + [c["agent"] for c in compatibility["can_feed_to"][:2]] + } + workflows.append(workflow) + + # Workflow 2: As a middle step + if compatibility["can_receive_from"] and compatibility["can_feed_to"]: + workflow = { + "name": f"{agent_name} in pipeline", + "description": f"Use {agent_name} as a processing step", + "agents": [ + compatibility["can_receive_from"][0]["agent"], + agent_name, + compatibility["can_feed_to"][0]["agent"] + ] + } + workflows.append(workflow) + + compatibility["suggested_workflows"] = workflows + + return compatibility + + def verify_registry_integrity(self) -> Dict[str, Any]: + """ + Verify integrity of registry files using provenance hashes. + + Returns: + Dictionary with verification results + """ + provenance = get_provenance_logger() + + results = { + "verified": [], + "failed": [], + "missing": [], + "summary": { + "total_checked": 0, + "verified_count": 0, + "failed_count": 0, + "missing_count": 0 + } + } + + # List of registry files to verify + registry_files = [ + ("skills.json", REGISTRY_FILE), + ("agents.json", str(Path(REGISTRY_DIR) / "agents.json")), + ("workflow_history.json", str(Path(REGISTRY_DIR) / "workflow_history.json")), + ] + + for artifact_id, file_path in registry_files: + results["summary"]["total_checked"] += 1 + + # Check if file exists + if not os.path.exists(file_path): + results["missing"].append({ + "artifact": artifact_id, + "path": file_path, + "reason": "File does not exist" + }) + results["summary"]["missing_count"] += 1 + continue + + try: + # Load the registry file + with open(file_path, 'r') as f: + content = json.load(f) + + # Get stored hash from file (if present) + stored_hash = content.get("content_hash") + + # Remove content_hash field to compute original hash + content_without_hash = {k: v for k, v in content.items() if k != "content_hash"} + + # Compute current hash + current_hash = compute_hash(content_without_hash) + + # Get latest hash from provenance log + latest_provenance_hash = provenance.get_latest_hash(artifact_id) + + # Verify + if stored_hash and stored_hash == current_hash: + # Hash matches what's in the file + verification_status = "verified" + + # Also check against provenance log + if latest_provenance_hash: + provenance_match = (stored_hash == latest_provenance_hash) + else: + provenance_match = None + + results["verified"].append({ + "artifact": artifact_id, + "path": file_path, + "hash": current_hash[:16] + "...", + "stored_hash_valid": True, + "provenance_logged": latest_provenance_hash is not None, + "provenance_match": provenance_match + }) + results["summary"]["verified_count"] += 1 + + elif stored_hash and stored_hash != current_hash: + # Hash mismatch - file may have been modified + results["failed"].append({ + "artifact": artifact_id, + "path": file_path, + "reason": "Content hash mismatch", + "stored_hash": stored_hash[:16] + "...", + "computed_hash": current_hash[:16] + "...", + "severity": "high" + }) + results["summary"]["failed_count"] += 1 + + else: + # No hash stored in file + results["missing"].append({ + "artifact": artifact_id, + "path": file_path, + "reason": "No content_hash field in file", + "computed_hash": current_hash[:16] + "...", + "provenance_available": latest_provenance_hash is not None + }) + results["summary"]["missing_count"] += 1 + + except Exception as e: + results["failed"].append({ + "artifact": artifact_id, + "path": file_path, + "reason": f"Verification error: {str(e)}", + "severity": "high" + }) + results["summary"]["failed_count"] += 1 + + return results + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="meta.compatibility - Agent Compatibility Analyzer" + ) + + subparsers = parser.add_subparsers(dest='command', help='Commands') + + # Find compatible command + find_parser = subparsers.add_parser('find-compatible', help='Find compatible agents') + find_parser.add_argument("agent", help="Agent name to analyze") + + # Suggest pipeline command + suggest_parser = subparsers.add_parser('suggest-pipeline', help='Suggest multi-agent pipeline') + suggest_parser.add_argument("goal", help="Goal description") + suggest_parser.add_argument("--artifacts", nargs="+", help="Required artifact types") + + # Analyze command + analyze_parser = subparsers.add_parser('analyze', help='Analyze agent compatibility') + analyze_parser.add_argument("agent", help="Agent name to analyze") + + # List all command + list_parser = subparsers.add_parser('list-all', help='List all compatibility') + + # Verify integrity command + verify_parser = subparsers.add_parser('verify-integrity', help='Verify registry integrity using provenance hashes') + + # Output format + parser.add_argument( + "--format", + choices=["json", "yaml", "text"], + default="text", + help="Output format" + ) + + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + analyzer = CompatibilityAnalyzer() + analyzer.scan_agents() + analyzer.build_compatibility_map() + + result = None + + if args.command == 'find-compatible': + print(f"🔍 Finding agents compatible with '{args.agent}'...\n") + result = analyzer.find_compatible(args.agent) + + if args.format == "text" and "error" not in result: + print(f"Agent: {result['agent']}") + print(f"Produces: {', '.join(result['produces']) if result['produces'] else 'none'}") + print(f"Consumes: {', '.join(result['consumes']) if result['consumes'] else 'none'}") + + if result['can_feed_to']: + print(f"\n✅ Can feed outputs to ({len(result['can_feed_to'])} agents):") + for comp in result['can_feed_to']: + print(f" • {comp['agent']} (via {comp['artifact']})") + + if result['can_receive_from']: + print(f"\n⬅️ Can receive inputs from ({len(result['can_receive_from'])} agents):") + for comp in result['can_receive_from']: + print(f" • {comp['agent']} (via {comp['artifact']})") + + if result['gaps']: + print(f"\n⚠️ Gaps ({len(result['gaps'])}):") + for gap in result['gaps']: + print(f" • {gap['artifact']}: {gap['issue']}") + + elif args.command == 'suggest-pipeline': + print(f"💡 Suggesting pipeline for: {args.goal}\n") + result = analyzer.suggest_pipeline(args.goal, args.artifacts) + + if args.format == "text" and "pipelines" in result: + for i, pipeline in enumerate(result["pipelines"], 1): + print(f"\n📋 Pipeline {i}: {pipeline['name']}") + print(f" {pipeline['description']}") + print(f" Complete: {'✅ Yes' if pipeline.get('complete', False) else '❌ No'}") + print(f" Steps:") + for step in pipeline['steps']: + print(f" {step['step']}. {step['agent']} - {step['description'][:60]}...") + + if pipeline.get('gaps'): + print(f" Gaps: {', '.join(pipeline['gaps'])}") + + elif args.command == 'analyze': + print(f"📊 Analyzing '{args.agent}'...\n") + result = analyzer.analyze_agent(args.agent) + + if args.format == "text" and "error" not in result: + print(f"Agent: {result['agent']}") + print(f"Produces: {', '.join(result['produces']) if result['produces'] else 'none'}") + print(f"Consumes: {', '.join(result['consumes']) if result['consumes'] else 'none'}") + + if result.get('suggested_workflows'): + print(f"\n🔄 Suggested Workflows:") + for workflow in result['suggested_workflows']: + print(f"\n {workflow['name']}") + print(f" {workflow['description']}") + print(f" Pipeline: {' → '.join(workflow['agents'])}") + + elif args.command == 'list-all': + print("🗺️ Generating complete compatibility graph...\n") + result = analyzer.generate_compatibility_graph() + + if args.format == "text": + print(f"Total Agents: {result['metadata']['total_agents']}") + print(f"Total Artifact Types: {result['metadata']['total_artifact_types']}") + print(f"Total Relationships: {len(result['relationships'])}") + + if result['gaps']: + print(f"\n⚠️ Global Gaps ({len(result['gaps'])}):") + for gap in result['gaps']: + print(f" • {gap['artifact']}: {gap['issue']}") + + elif args.command == 'verify-integrity': + print("🔐 Verifying registry integrity using provenance hashes...\n") + result = analyzer.verify_registry_integrity() + + if args.format == "text": + summary = result['summary'] + print(f"Total Checked: {summary['total_checked']}") + print(f"✅ Verified: {summary['verified_count']}") + print(f"❌ Failed: {summary['failed_count']}") + print(f"⚠️ Missing Hash: {summary['missing_count']}") + + if result['verified']: + print(f"\n✅ Verified Artifacts ({len(result['verified'])}):") + for item in result['verified']: + print(f" • {item['artifact']}: {item['hash']}") + if item.get('provenance_logged'): + match_status = "✓" if item.get('provenance_match') else "✗" + print(f" Provenance: {match_status}") + + if result['failed']: + print(f"\n❌ Failed Verifications ({len(result['failed'])}):") + for item in result['failed']: + print(f" • {item['artifact']}: {item['reason']}") + if 'stored_hash' in item: + print(f" Expected: {item['stored_hash']}") + print(f" Computed: {item['computed_hash']}") + + if result['missing']: + print(f"\n⚠️ Missing Hashes ({len(result['missing'])}):") + for item in result['missing']: + print(f" • {item['artifact']}: {item['reason']}") + + # Output result + if result: + if args.format == "json": + print(json.dumps(result, indent=2)) + elif args.format == "yaml": + print(yaml.dump(result, default_flow_style=False)) + elif "error" in result: + print(f"\n❌ Error: {result['error']}") + if "suggestion" in result: + print(f"💡 {result['suggestion']}") + + +if __name__ == "__main__": + main() diff --git a/agents/meta.config.router/README.md b/agents/meta.config.router/README.md new file mode 100644 index 0000000..eab10d7 --- /dev/null +++ b/agents/meta.config.router/README.md @@ -0,0 +1,247 @@ +# Agent: meta.config.router + +## Purpose + +Configure Claude Code Router for Betty to support multi-model LLM routing across environments. This agent creates or previews a `config.json` file at `~/.claude-code-router/config.json` with model providers, routing profiles, and audit metadata. + +## Version + +0.1.0 + +## Status + +active + +## Reasoning Mode + +oneshot + +## Capabilities + +- Generate multi-model LLM router configurations +- Validate router configuration inputs for correctness +- Apply configurations to filesystem with audit trails +- Support multiple output modes (preview, file, both) +- Work across local, cloud, and CI environments +- Ensure deterministic and portable configurations + +## Skills Available + +- `config.validate.router` - Validates router configuration inputs +- `config.generate.router` - Generates router configuration JSON +- `audit.log` - Records audit events for configuration changes + +## Inputs + +### llm_backends (required) +- **Type**: List of objects +- **Description**: Backend provider configurations +- **Schema**: + ```json + [ + { + "name": "string (e.g., openrouter, ollama, claude)", + "api_base_url": "string (API endpoint URL)", + "api_key": "string (optional for local providers)", + "models": ["string (model identifiers)"] + } + ] + ``` + +### routing_rules (required) +- **Type**: Dictionary +- **Description**: Mapping of Claude routing contexts to provider/model pairs +- **Contexts**: default, think, background, longContext +- **Schema**: + ```json + { + "default": { "provider": "string", "model": "string" }, + "think": { "provider": "string", "model": "string" }, + "background": { "provider": "string", "model": "string" }, + "longContext": { "provider": "string", "model": "string" } + } + ``` + +### output_mode (optional) +- **Type**: enum +- **Values**: "preview" | "file" | "both" +- **Default**: "preview" +- **Description**: Output mode for configuration + +### apply_config (optional) +- **Type**: boolean +- **Default**: false +- **Description**: Write config to disk if true + +### metadata (optional) +- **Type**: object +- **Description**: Optional audit metadata (initiator, environment, etc.) + +## Outputs + +### routing_config +- **Type**: object +- **Description**: Rendered router config as JSON + +### write_status +- **Type**: string +- **Values**: "success" | "skipped" | "error" +- **Description**: Status of file write operation + +### audit_id +- **Type**: string +- **Description**: Unique trace ID for configuration event + +## Behavior + +1. Validates inputs via `config.validate.router` +2. Constructs valid router config using `config.generate.router` +3. If `apply_config=true` and `output_mode≠preview`, writes config to: `~/.claude-code-router/config.json` +4. Outputs JSON config regardless of write action +5. Logs audit record via `audit.log` with: + - timestamp + - initiator + - hash of input + - environment fingerprint + +## Usage Example + +```bash +# Preview configuration (no file write) +/meta/config.router --routing_config_path=router-config.yaml + +# Apply configuration to disk +/meta/config.router --routing_config_path=router-config.yaml --apply_config=true + +# Both preview and write +/meta/config.router --routing_config_path=router-config.yaml --apply_config=true --output_mode=both +``` + +## Example Input (YAML) + +```yaml +llm_backends: + - name: openrouter + api_base_url: https://openrouter.ai/api/v1 + api_key: ${OPENROUTER_API_KEY} + models: + - anthropic/claude-3.5-sonnet + - openai/gpt-4 + + - name: ollama + api_base_url: http://localhost:11434/v1 + models: + - llama3.1:70b + - codellama:34b + +routing_rules: + default: + provider: openrouter + model: anthropic/claude-3.5-sonnet + + think: + provider: openrouter + model: anthropic/claude-3.5-sonnet + + background: + provider: ollama + model: llama3.1:70b + + longContext: + provider: openrouter + model: anthropic/claude-3.5-sonnet + +metadata: + initiator: user@example.com + environment: production + purpose: Multi-model routing for development +``` + +## Example Output + +```json +{ + "version": "1.0.0", + "generated_at": "2025-11-01T12:34:56Z", + "backends": [ + { + "name": "openrouter", + "api_base_url": "https://openrouter.ai/api/v1", + "api_key": "${OPENROUTER_API_KEY}", + "models": [ + "anthropic/claude-3.5-sonnet", + "openai/gpt-4" + ] + }, + { + "name": "ollama", + "api_base_url": "http://localhost:11434/v1", + "models": [ + "llama3.1:70b", + "codellama:34b" + ] + } + ], + "routing": { + "default": { + "provider": "openrouter", + "model": "anthropic/claude-3.5-sonnet" + }, + "think": { + "provider": "openrouter", + "model": "anthropic/claude-3.5-sonnet" + }, + "background": { + "provider": "ollama", + "model": "llama3.1:70b" + }, + "longContext": { + "provider": "openrouter", + "model": "anthropic/claude-3.5-sonnet" + } + }, + "metadata": { + "generated_by": "meta.config.router", + "schema_version": "1.0.0", + "initiator": "user@example.com", + "environment": "production", + "purpose": "Multi-model routing for development" + } +} +``` + +## Permissions + +- `filesystem:read` - Read router config input files +- `filesystem:write` - Write config to ~/.claude-code-router/config.json + +## Artifacts + +### Consumes +- `router-config-input` - User-provided router configuration inputs + +### Produces +- `llm-router-config` - Complete Claude Code Router configuration file +- `audit-log-entry` - Audit trail entry for configuration events + +## Tags + +llm, router, configuration, meta, infra, openrouter, claude, ollama, multi-model + +## Environments + +- local +- cloud +- ci + +## Requires Human Approval + +false + +## Notes + +- The config is deterministic and portable across environments +- API keys can use environment variable substitution (e.g., ${OPENROUTER_API_KEY}) +- Local providers (localhost/127.0.0.1) don't require API keys +- All configuration changes are audited for traceability +- The agent supports preview mode to verify configuration before applying diff --git a/agents/meta.config.router/agent.yaml b/agents/meta.config.router/agent.yaml new file mode 100644 index 0000000..c7f12d4 --- /dev/null +++ b/agents/meta.config.router/agent.yaml @@ -0,0 +1,92 @@ +name: meta.config.router +version: 0.1.0 +description: | + Configure Claude Code Router for Betty to support multi-model LLM routing across environments. + This agent creates or previews a config.json file at ~/.claude-code-router/config.json with + model providers, routing profiles, and audit metadata. Works across local, cloud, or CI-based + environments with built-in validation, output rendering, config application, and auditing. +status: active +reasoning_mode: oneshot + +capabilities: + - Generate multi-model LLM router configurations + - Validate router configuration inputs for correctness + - Apply configurations to filesystem with audit trails + - Support multiple output modes (preview, file, both) + - Work across local, cloud, and CI environments + - Ensure deterministic and portable configurations + +skills_available: + - config.validate.router + - config.generate.router + - audit.log + +permissions: + - filesystem:read + - filesystem:write + +artifact_metadata: + consumes: + - type: router-config-input + description: User-provided router configuration inputs (backends, routing rules, metadata) + file_pattern: "*-router-input.{json,yaml}" + content_type: application/json + required: true + + produces: + - type: llm-router-config + description: Complete Claude Code Router configuration file + file_pattern: "config.json" + content_type: application/json + schema: schemas/router-config.json + + - type: audit-log-entry + description: Audit trail entry for configuration events + file_pattern: "audit_log.json" + content_type: application/json + +system_prompt: | + You are the meta.config.router agent for the Betty Framework. + + Your responsibilities: + 1. Validate router configuration inputs using config.validate.router + 2. Generate valid router config JSON using config.generate.router + 3. Write config to ~/.claude-code-router/config.json when apply_config=true + 4. Provide preview, file write, or both modes based on output_mode + 5. Log audit records with timestamp, initiator, and environment fingerprint + + Inputs you expect: + - llm_backends: List of provider configs (name, api_base_url, api_key, models) + - routing_rules: Mapping of routing contexts (default, think, background, longContext) + - output_mode: "preview" | "file" | "both" (default: preview) + - apply_config: boolean (write to disk if true) + - metadata: Optional audit metadata (initiator, environment, etc.) + + Outputs you generate: + - routing_config: Complete router configuration JSON + - write_status: "success" | "skipped" | "error" + - audit_id: Unique trace ID for the configuration event + + Workflow: + 1. Call config.validate.router with llm_backends and routing_rules + 2. If validation fails, return errors and exit + 3. Call config.generate.router to create the config JSON + 4. If apply_config=true and output_mode≠preview, write to ~/.claude-code-router/config.json + 5. Call audit.log to record the configuration event + 6. Return config, write status, and audit ID + + Environment awareness: + - Detect local vs cloud vs CI environment + - Adjust file paths accordingly + - Include environment fingerprint in audit metadata + +tags: + - llm + - router + - configuration + - meta + - infra + - openrouter + - claude + - ollama + - multi-model diff --git a/agents/meta.config.router/meta_config_router.py b/agents/meta.config.router/meta_config_router.py new file mode 100755 index 0000000..24d8324 --- /dev/null +++ b/agents/meta.config.router/meta_config_router.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +""" +Agent: meta.config.router +Configure Claude Code Router for multi-model LLM support +""" + +import json +import sys +import os +import hashlib +import uuid +from datetime import datetime +from pathlib import Path +from typing import Dict, Any, List +import subprocess +import yaml + + +class MetaConfigRouter: + """Configure Claude Code Router for Betty Framework""" + + def __init__(self): + self.betty_root = Path(__file__).parent.parent.parent + self.skills_root = self.betty_root / "skills" + self.audit_log_path = self.betty_root / "registry" / "audit_log.json" + + def run( + self, + routing_config_path: str, + apply_config: bool = False, + output_mode: str = "preview" + ) -> Dict[str, Any]: + """ + Main execution method + + Args: + routing_config_path: Path to router config input file (YAML or JSON) + apply_config: Whether to write config to disk + output_mode: "preview" | "file" | "both" + + Returns: + Result with routing_config, write_status, and audit_id + """ + print(f"🔧 meta.config.router v0.1.0") + print(f"📋 Config input: {routing_config_path}") + print(f"📝 Output mode: {output_mode}") + print(f"💾 Apply config: {apply_config}") + print() + + # Load input config + config_input = self._load_config_input(routing_config_path) + + # Extract inputs + llm_backends = config_input.get("llm_backends", []) + routing_rules = config_input.get("routing_rules", {}) + config_options = config_input.get("config_options", {}) + metadata = config_input.get("metadata", {}) # For audit logging only + + # Step 1: Validate inputs + print("🔍 Validating router configuration...") + validation_result = self._validate_config(llm_backends, routing_rules) + + if not validation_result["valid"]: + print("❌ Validation failed:") + for error in validation_result["errors"]: + print(f" - {error}") + return { + "success": False, + "errors": validation_result["errors"], + "warnings": validation_result["warnings"] + } + + if validation_result["warnings"]: + print("⚠️ Warnings:") + for warning in validation_result["warnings"]: + print(f" - {warning}") + + print("✅ Validation passed") + print() + + # Step 2: Generate router config + print("🏗️ Generating router configuration...") + router_config = self._generate_config( + llm_backends, + routing_rules, + config_options + ) + print("✅ Configuration generated") + print() + + # Step 3: Write config if requested + write_status = "skipped" + config_path = None + + if apply_config and output_mode != "preview": + print("💾 Writing configuration to disk...") + config_path, write_status = self._write_config(router_config) + + if write_status == "success": + print(f"✅ Configuration written to: {config_path}") + else: + print(f"❌ Failed to write configuration") + print() + + # Step 4: Log audit record + print("📝 Logging audit record...") + audit_id = self._log_audit( + config_input=config_input, + write_status=write_status, + metadata=metadata + ) + print(f"✅ Audit ID: {audit_id}") + print() + + # Step 5: Output results + result = { + "success": True, + "routing_config": router_config, + "write_status": write_status, + "audit_id": audit_id + } + + if config_path: + result["config_path"] = str(config_path) + + # Display preview if requested + if output_mode in ["preview", "both"]: + print("📄 Router Configuration Preview:") + print("─" * 80) + print(json.dumps(router_config, indent=2)) + print("─" * 80) + print() + + return result + + def _load_config_input(self, config_path: str) -> Dict[str, Any]: + """Load router config input from YAML or JSON file""" + path = Path(config_path) + + if not path.exists(): + raise FileNotFoundError(f"Config file not found: {config_path}") + + with open(path, 'r') as f: + if path.suffix in ['.yaml', '.yml']: + return yaml.safe_load(f) + else: + return json.load(f) + + def _validate_config( + self, + llm_backends: List[Dict[str, Any]], + routing_rules: Dict[str, Any] + ) -> Dict[str, Any]: + """Validate router configuration using config.validate.router skill""" + validator_script = self.skills_root / "config.validate.router" / "validate_router.py" + + config_json = json.dumps({ + "llm_backends": llm_backends, + "routing_rules": routing_rules + }) + + try: + result = subprocess.run( + [sys.executable, str(validator_script), config_json], + capture_output=True, + text=True, + check=False + ) + + return json.loads(result.stdout) + except Exception as e: + return { + "valid": False, + "errors": [f"Validation error: {e}"], + "warnings": [] + } + + def _generate_config( + self, + llm_backends: List[Dict[str, Any]], + routing_rules: Dict[str, Any], + config_options: Dict[str, Any] + ) -> Dict[str, Any]: + """Generate router configuration using config.generate.router skill""" + generator_script = self.skills_root / "config.generate.router" / "generate_router.py" + + input_json = json.dumps({ + "llm_backends": llm_backends, + "routing_rules": routing_rules, + "config_options": config_options + }) + + try: + result = subprocess.run( + [sys.executable, str(generator_script), input_json], + capture_output=True, + text=True, + check=True + ) + + return json.loads(result.stdout) + except Exception as e: + raise RuntimeError(f"Config generation failed: {e}") + + def _write_config(self, router_config: Dict[str, Any]) -> tuple[Path, str]: + """Write router config to ~/.claude-code-router/config.json""" + try: + config_dir = Path.home() / ".claude-code-router" + config_dir.mkdir(parents=True, exist_ok=True) + + config_path = config_dir / "config.json" + + with open(config_path, 'w') as f: + json.dump(router_config, f, indent=2) + + return config_path, "success" + except Exception as e: + print(f"Error writing config: {e}") + return None, "error" + + def _log_audit( + self, + config_input: Dict[str, Any], + write_status: str, + metadata: Dict[str, Any] + ) -> str: + """Log audit record for configuration event""" + audit_id = str(uuid.uuid4()) + + # Calculate hash of input + input_hash = hashlib.sha256( + json.dumps(config_input, sort_keys=True).encode() + ).hexdigest()[:16] + + audit_entry = { + "audit_id": audit_id, + "timestamp": datetime.utcnow().isoformat() + "Z", + "agent": "meta.config.router", + "version": "0.1.0", + "action": "router_config_generated", + "write_status": write_status, + "input_hash": input_hash, + "environment": self._detect_environment(), + "initiator": metadata.get("initiator", "unknown"), + "metadata": metadata + } + + # Append to audit log + try: + if self.audit_log_path.exists(): + with open(self.audit_log_path, 'r') as f: + audit_log = json.load(f) + else: + audit_log = [] + + audit_log.append(audit_entry) + + with open(self.audit_log_path, 'w') as f: + json.dump(audit_log, f, indent=2) + except Exception as e: + print(f"Warning: Failed to write audit log: {e}") + + return audit_id + + def _detect_environment(self) -> str: + """Detect execution environment (local, cloud, ci)""" + if os.getenv("CI"): + return "ci" + elif os.getenv("CLOUD_ENV"): + return "cloud" + else: + return "local" + + +def main(): + """CLI entrypoint""" + if len(sys.argv) < 2: + print("Usage: meta_config_router.py [--apply_config] [--output_mode=]") + print() + print("Arguments:") + print(" routing_config_path Path to router config input file (YAML or JSON)") + print(" --apply_config Write config to ~/.claude-code-router/config.json") + print(" --output_mode=MODE Output mode: preview, file, or both (default: preview)") + sys.exit(1) + + # Parse arguments + routing_config_path = sys.argv[1] + apply_config = "--apply_config" in sys.argv or "--apply-config" in sys.argv + output_mode = "preview" + + for arg in sys.argv[2:]: + if arg.startswith("--output_mode=") or arg.startswith("--output-mode="): + output_mode = arg.split("=")[1] + + # Run agent + agent = MetaConfigRouter() + try: + result = agent.run( + routing_config_path=routing_config_path, + apply_config=apply_config, + output_mode=output_mode + ) + + if result["success"]: + print("✅ meta.config.router completed successfully") + print(f"📋 Audit ID: {result['audit_id']}") + print(f"💾 Write status: {result['write_status']}") + sys.exit(0) + else: + print("❌ meta.config.router failed") + for error in result.get("errors", []): + print(f" - {error}") + sys.exit(1) + + except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/agents/meta.create/README.md b/agents/meta.create/README.md new file mode 100644 index 0000000..b94bb2a --- /dev/null +++ b/agents/meta.create/README.md @@ -0,0 +1,374 @@ +# meta.create - Component Creation Orchestrator + +The intelligent orchestrator for creating Betty skills, commands, and agents from natural language descriptions. + +## Purpose + +`meta.create` is the primary entry point for creating Betty components. It automatically: + +- **Detects** what type of component you're describing (skill, command, agent, or combination) +- **Checks** inventory to avoid duplicates +- **Analyzes** complexity to determine the optimal creation pattern +- **Creates** components in dependency order +- **Validates** compatibility and identifies gaps +- **Recommends** next steps for completion + +## Why Use meta.create? + +Instead of manually running multiple meta-agents (`meta.skill`, `meta.command`, `meta.agent`, `meta.compatibility`), `meta.create` orchestrates everything for you in the right order. + +### Before meta.create: +```bash +# Manual workflow - you had to know the order and check everything +python3 agents/meta.command/meta_command.py description.md +# Check if it recommends creating a skill... +python3 agents/meta.skill/meta_skill.py skill_description.md +python3 agents/meta.agent/meta_agent.py agent_description.md +python3 agents/meta.compatibility/meta_compatibility.py analyze my.agent +# Check for gaps, create missing skills... +``` + +### With meta.create: +```bash +# One command does it all +python3 agents/meta.create/meta_create.py description.md +``` + +## How It Works + +### Step 1: Analysis +Parses your description to determine: +- Is this a skill? command? agent? +- What artifacts are involved? +- What's the complexity level? + +### Step 2: Duplicate Check +Queries registries to find existing components: +- Prevents recreating existing skills +- Shows what you can reuse +- Skips unnecessary work + +### Step 3: Creation Planning +Uses `meta.command` complexity analysis to determine pattern: +- **COMMAND_ONLY**: Simple inline logic (1-3 steps) +- **SKILL_ONLY**: Reusable utility without command +- **SKILL_AND_COMMAND**: Complex logic in skill + command wrapper +- **AGENT**: Multi-skill orchestration + +### Step 4: Component Creation +Creates components in dependency order: +1. **Skills first** (using `meta.skill`) +2. **Commands second** (using `meta.command`) +3. **Agents last** (using `meta.agent` with skill composition) + +### Step 5: Compatibility Validation +For agents, runs `meta.compatibility` to: +- Find compatible agent pipelines +- Identify artifact gaps +- Suggest workflows + +### Step 6: Recommendations +Provides actionable next steps: +- Missing skills to create +- Compatibility issues to fix +- Integration opportunities + +## Usage + +### Basic Usage + +```bash +python3 agents/meta.create/meta_create.py +``` + +### Create Skill and Command + +```bash +python3 agents/meta.create/meta_create.py examples/api_validate.md +``` + +If `api_validate.md` describes a complex command, meta.create will: +1. Analyze complexity → detects SKILL_AND_COMMAND pattern +2. Create the skill first +3. Create the command that uses the skill +4. Report what was created + +### Create Agent with Dependencies + +```bash +python3 agents/meta.create/meta_create.py examples/api_agent.md +``` + +meta.create will: +1. Detect it's an agent description +2. Check for required skills (reuse existing) +3. Create missing skills if needed +4. Create the agent with proper skill composition +5. Validate compatibility with other agents +6. Report gaps and recommendations + +### Auto-Fill Gaps + +```bash +python3 agents/meta.create/meta_create.py description.md --auto-fill-gaps +``` + +Automatically creates missing skills to fill compatibility gaps. + +### Skip Duplicate Check + +```bash +python3 agents/meta.create/meta_create.py description.md --skip-duplicate-check +``` + +Force creation even if components exist (useful for updates). + +### Output Formats + +```bash +# Human-readable text (default) +python3 agents/meta.create/meta_create.py description.md + +# JSON output for automation +python3 agents/meta.create/meta_create.py description.md --output-format json + +# YAML output +python3 agents/meta.create/meta_create.py description.md --output-format yaml +``` + +### With Traceability + +```bash +python3 agents/meta.create/meta_create.py description.md \ + --requirement-id REQ-2025-042 \ + --requirement-description "Create API validation agent" \ + --issue-id JIRA-1234 \ + --requested-by "Product Team" +``` + +## Description File Format + +Your description file can be Markdown or JSON. meta.create detects the type automatically. + +### Example: Skill Description + +```markdown +# Name: data.validate + +# Type: skill + +# Purpose: +Validate data against JSON schemas with detailed error reporting + +# Inputs: +- data (JSON object to validate) +- schema (JSON schema for validation) + +# Outputs: +- validation_result (validation report with errors) + +# Produces Artifacts: +- validation.report + +# Consumes Artifacts: +- data.json +- schema.json +``` + +### Example: Command Description + +```markdown +# Name: /validate-api + +# Type: command + +# Description: +Validate API responses against OpenAPI schemas + +# Execution Type: skill + +# Target: api.validate + +# Parameters: +- endpoint: string (required) - API endpoint to validate +- schema: string (required) - Path to OpenAPI schema +``` + +### Example: Agent Description + +```markdown +# Name: api.validator + +# Type: agent + +# Purpose: +Comprehensive API testing and validation agent + +# Inputs: +- api.spec + +# Outputs: +- validation.report +- test.results + +# Examples: +- Validate all API endpoints against OpenAPI spec +- Generate test cases from schema +``` + +## What Gets Created + +### For Skills +- `skills/{name}/skill.yaml` - Skill configuration +- `skills/{name}/{name}.py` - Python implementation stub +- `skills/{name}/test_{name}.py` - pytest test template +- `skills/{name}/README.md` - Documentation + +### For Commands +- `commands/{name}.yaml` - Command manifest +- Recommendations for skill creation if needed + +### For Agents +- `agents/{name}/agent.yaml` - Agent configuration +- `agents/{name}/README.md` - Documentation with usage examples +- Compatibility analysis report + +## Output Report + +meta.create provides a comprehensive report: + +``` +🎯 meta.create - Orchestrating component creation from description.md + +📋 Step 1: Analyzing description... + Detected types: Skill=True, Command=True, Agent=False + +🔍 Step 2: Checking for existing components... + ✅ No duplicates found + +🛠️ Step 3: Creating components... + 📊 Analyzing command complexity... + Recommended pattern: SKILL_AND_COMMAND + Should create skill: True + + 🔧 Creating skill... + ✅ Skill 'api.validate' created + + 📜 Creating command... + ✅ Command '/validate-api' created + +================================================================================ +✨ CREATION SUMMARY +================================================================================ + +✅ Created 2 component(s): + • SKILL: api.validate + • COMMAND: /validate-api + +================================================================================ +``` + +## Integration with Other Meta-Agents + +meta.create uses: +- **meta.command** - Complexity analysis and command generation +- **meta.skill** - Skill creation with full package +- **meta.agent** - Agent creation with skill composition +- **meta.compatibility** - Compatibility validation and gap detection +- **registry.query** - Duplicate checking +- **agent.compose** - Skill recommendation for agents + +## Decision Tree + +``` +Description Input + ↓ + Parse Type + ↓ + ┌──┴──────────────────┐ + ↓ ↓ + Command? Agent? + ↓ ↓ +Analyze Find Skills +Complexity ↓ + ↓ Create Missing +SKILL_ONLY Skills +COMMAND_ONLY ↓ +SKILL_AND_COMMAND Create Agent + ↓ ↓ +Create Skill Validate Compat + ↓ ↓ +Create Command Report Gaps + ↓ ↓ + Done Recommend +``` + +## Examples + +### Example 1: Simple Command + +```bash +# description.md specifies a simple 2-step command +python3 agents/meta.create/meta_create.py description.md +# Result: Creates COMMAND_ONLY (inline logic is sufficient) +``` + +### Example 2: Complex Command + +```bash +# description.md specifies 10+ step validation logic +python3 agents/meta.create/meta_create.py description.md +# Result: Creates SKILL_AND_COMMAND (skill has logic, command delegates) +``` + +### Example 3: Multi-Agent System + +```bash +# description.md describes an orchestration agent +python3 agents/meta.create/meta_create.py description.md +# Result: +# - Creates agent with existing skills +# - Validates compatibility +# - Reports: "Can receive from api.architect, can feed to report.generator" +# - Suggests pipeline workflows +``` + +## Benefits + +✅ **Intelligent** - Automatically determines optimal creation pattern +✅ **Safe** - Checks for duplicates, prevents overwrites +✅ **Complete** - Creates all necessary components in order +✅ **Validated** - Runs compatibility checks automatically +✅ **Traceable** - Supports requirement tracking +✅ **Informative** - Provides detailed reports and recommendations + +## Next Steps + +After using meta.create: + +1. **Review** created files +2. **Implement** TODO sections in generated code +3. **Test** with pytest +4. **Register** components (manual or use `skill.register`, etc.) +5. **Use** in your Betty workflows + +## Troubleshooting + +**Q: meta.create says component already exists** +A: Use `--skip-duplicate-check` to override, or rename your component + +**Q: Compatibility gaps reported** +A: Use `--auto-fill-gaps` or manually create the missing skills + +**Q: Wrong pattern detected** +A: Add explicit `# Type: skill` or `# Type: command` to your description + +## Related Documentation + +- [META_AGENTS.md](../../docs/META_AGENTS.md) - Overview of all meta-agents +- [SKILL_COMMAND_DECISION_TREE.md](../../docs/SKILL_COMMAND_DECISION_TREE.md) - Pattern decision logic +- [ARTIFACTS.md](../../docs/ARTIFACTS.md) - Artifact metadata system + +--- + +*Created by the Betty Framework Meta-Agent System* diff --git a/agents/meta.create/agent.yaml b/agents/meta.create/agent.yaml new file mode 100644 index 0000000..22db0d0 --- /dev/null +++ b/agents/meta.create/agent.yaml @@ -0,0 +1,80 @@ +name: meta.create +version: 0.1.0 +description: | + Orchestrator meta-agent that intelligently creates skills, commands, and agents. + + Capabilities: + - Detects component type from description + - Checks inventory for duplicates + - Analyzes complexity and determines creation pattern + - Creates skills, commands, and agents in proper order + - Validates compatibility using meta.compatibility + - Identifies gaps and provides recommendations + - Supports auto-filling missing dependencies + + This is the primary entry point for creating Betty components from natural + language descriptions. + +status: draft +reasoning_mode: iterative +capabilities: + - Diagnose component needs and recommend skills, commands, or agents to create + - Generate scaffolding for new framework components with proper metadata + - Coordinate validation steps to ensure compatibility before registration + +skills_available: + - registry.query + - agent.compose + +permissions: + - filesystem:read + - filesystem:write + - registry:read + - registry:write + +artifact_metadata: + consumes: + - type: component.description + description: Natural language description of component to create + format: markdown or JSON + required: true + + produces: + - type: skill.definition + description: Complete skill package with YAML, implementation, tests + optional: true + + - type: command.manifest + description: Command manifest in YAML format + optional: true + + - type: agent.definition + description: Agent configuration with skill composition + optional: true + + - type: compatibility.report + description: Compatibility analysis showing agent relationships and gaps + optional: true + +tags: + - meta + - orchestration + - creation + - automation + +system_prompt: | + You are meta.create, the intelligent orchestrator for creating Betty components. + + Your responsibilities: + 1. Analyze component descriptions to determine type (skill/command/agent) + 2. Check registries to avoid creating duplicates + 3. Determine optimal creation pattern using complexity analysis + 4. Create components in dependency order (skills → commands → agents) + 5. Validate agent compatibility and identify gaps + 6. Provide actionable recommendations for completion + + Always prioritize: + - Reusing existing components over creating new ones + - Creating building blocks (skills) before orchestrators (agents) + - Validating compatibility to ensure smooth agent pipelines + - Providing clear feedback about what was created and why diff --git a/agents/meta.create/meta_create.py b/agents/meta.create/meta_create.py new file mode 100644 index 0000000..9a87dd0 --- /dev/null +++ b/agents/meta.create/meta_create.py @@ -0,0 +1,555 @@ +#!/usr/bin/env python3 +""" +meta.create - Orchestrator Meta-Agent + +Intelligently orchestrates the creation of skills, commands, and agents. +Checks inventory, determines what needs to be created, validates compatibility, +and fills gaps automatically. + +This is the main entry point for creating Betty components from descriptions. +""" + +import json +import yaml +import sys +import os +from pathlib import Path +from typing import Dict, List, Any, Optional, Set, Tuple +from datetime import datetime + +# Add parent directory to path for imports +parent_dir = str(Path(__file__).parent.parent.parent) +sys.path.insert(0, parent_dir) + +# Import other meta agents by adding their paths +meta_command_path = Path(parent_dir) / "agents" / "meta.command" +meta_skill_path = Path(parent_dir) / "agents" / "meta.skill" +meta_agent_path = Path(parent_dir) / "agents" / "meta.agent" +meta_compatibility_path = Path(parent_dir) / "agents" / "meta.compatibility" +registry_query_path = Path(parent_dir) / "skills" / "registry.query" + +sys.path.insert(0, str(meta_command_path)) +sys.path.insert(0, str(meta_skill_path)) +sys.path.insert(0, str(meta_agent_path)) +sys.path.insert(0, str(meta_compatibility_path)) +sys.path.insert(0, str(registry_query_path)) + +import meta_command +import meta_skill +import meta_agent +import meta_compatibility +import registry_query + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.traceability import get_tracer, RequirementInfo + +logger = setup_logger(__name__) + + +class ComponentCreator: + """Orchestrates the creation of skills, commands, and agents""" + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize orchestrator""" + self.base_dir = Path(base_dir) + self.created_components = [] + self.compatibility_analyzer = None + + def check_duplicate(self, component_type: str, name: str) -> Optional[Dict[str, Any]]: + """ + Check if a component already exists in registry + + Args: + component_type: 'skills', 'commands', or 'agents' + name: Component name to check + + Returns: + Existing component info if found, None otherwise + """ + try: + result = registry_query.query_registry( + registry=component_type, + name=name, + fuzzy=False + ) + + if result.get("ok") and result.get("details", {}).get("matching_entries", 0) > 0: + matches = result["details"]["results"] + # Check for exact match + for match in matches: + if match["name"] == name: + return match + return None + + except Exception as e: + logger.warning(f"Error checking duplicate for {name}: {e}") + return None + + def parse_description_type(self, description_path: str) -> Dict[str, Any]: + """ + Determine what type of component is being described + + Args: + description_path: Path to description file + + Returns: + Dict with component_type and parsed metadata + """ + path = Path(description_path) + content = path.read_text() + + # Try to determine type from content + result = { + "is_skill": False, + "is_command": False, + "is_agent": False, + "path": str(path) + } + + content_lower = content.lower() + + # Check for skill indicators + if any(x in content_lower for x in ["# produces artifacts:", "# consumes artifacts:", + "skill.yaml", "artifact_metadata"]): + result["is_skill"] = True + + # Check for command indicators + if any(x in content_lower for x in ["# execution type:", "# parameters:", + "command manifest"]): + result["is_command"] = True + + # Check for agent indicators + if any(x in content_lower for x in ["# skills:", "skills_available", + "agent purpose", "multi-step", "orchestrat"]): + result["is_agent"] = True + + # If ambiguous, look at explicit markers + if "# type: skill" in content_lower: + result["is_skill"] = True + result["is_command"] = False + result["is_agent"] = False + elif "# type: command" in content_lower: + result["is_command"] = True + result["is_skill"] = False + result["is_agent"] = False + elif "# type: agent" in content_lower: + result["is_agent"] = True + result["is_skill"] = False + result["is_command"] = False + + return result + + def create_skill( + self, + description_path: str, + requirement: Optional[RequirementInfo] = None + ) -> Dict[str, Any]: + """ + Create a skill using meta.skill + + Args: + description_path: Path to skill description + requirement: Optional requirement info + + Returns: + Creation result + """ + logger.info(f"Creating skill from {description_path}") + + creator = meta_skill.SkillCreator(base_dir=str(self.base_dir)) + result = creator.create_skill(description_path, requirement=requirement) + + self.created_components.append({ + "type": "skill", + "name": result.get("skill_name"), + "files": result.get("created_files", []), + "trace_id": result.get("trace_id") + }) + + return result + + def create_command( + self, + description_path: str, + requirement: Optional[RequirementInfo] = None + ) -> Dict[str, Any]: + """ + Create a command using meta.command + + Args: + description_path: Path to command description + requirement: Optional requirement info + + Returns: + Creation result with complexity analysis + """ + logger.info(f"Creating command from {description_path}") + + creator = meta_command.CommandCreator(base_dir=str(self.base_dir)) + result = creator.create_command(description_path, requirement=requirement) + + self.created_components.append({ + "type": "command", + "name": result.get("command_name"), + "manifest": result.get("manifest_file"), + "analysis": result.get("complexity_analysis"), + "trace_id": result.get("trace_id") + }) + + return result + + def create_agent( + self, + description_path: str, + requirement: Optional[RequirementInfo] = None + ) -> Dict[str, Any]: + """ + Create an agent using meta.agent + + Args: + description_path: Path to agent description + requirement: Optional requirement info + + Returns: + Creation result + """ + logger.info(f"Creating agent from {description_path}") + + creator = meta_agent.AgentCreator( + registry_path=str(self.base_dir / "registry" / "skills.json") + ) + result = creator.create_agent(description_path, requirement=requirement) + + self.created_components.append({ + "type": "agent", + "name": result.get("name"), + "files": [result.get("agent_yaml"), result.get("readme")], + "skills": result.get("skills", []), + "trace_id": result.get("trace_id") + }) + + return result + + def validate_compatibility(self, agent_name: str) -> Dict[str, Any]: + """ + Validate agent compatibility using meta.compatibility + + Args: + agent_name: Name of agent to validate + + Returns: + Compatibility analysis + """ + logger.info(f"Validating compatibility for {agent_name}") + + if not self.compatibility_analyzer: + self.compatibility_analyzer = meta_compatibility.CompatibilityAnalyzer( + base_dir=str(self.base_dir) + ) + self.compatibility_analyzer.scan_agents() + self.compatibility_analyzer.build_compatibility_map() + + return self.compatibility_analyzer.analyze_agent(agent_name) + + def orchestrate_creation( + self, + description_path: str, + auto_fill_gaps: bool = False, + check_duplicates: bool = True, + requirement: Optional[RequirementInfo] = None + ) -> Dict[str, Any]: + """ + Main orchestration method that intelligently creates components + + Args: + description_path: Path to description file + auto_fill_gaps: Whether to automatically create missing dependencies + check_duplicates: Whether to check for existing components + requirement: Optional requirement info for traceability + + Returns: + Comprehensive creation report + """ + print(f"🎯 meta.create - Orchestrating component creation from {description_path}\n") + + report = { + "ok": True, + "description_path": description_path, + "component_type": None, + "created_components": [], + "skipped_components": [], + "compatibility_analysis": None, + "gaps": [], + "recommendations": [], + "errors": [] + } + + try: + # Step 1: Determine what's being described + print("📋 Step 1: Analyzing description...") + desc_type = self.parse_description_type(description_path) + print(f" Detected types: Skill={desc_type['is_skill']}, " + f"Command={desc_type['is_command']}, Agent={desc_type['is_agent']}\n") + + # Step 2: Check for duplicates if requested + if check_duplicates: + print("🔍 Step 2: Checking for existing components...") + + # Parse name from description + content = Path(description_path).read_text() + name_match = None + for line in content.split('\n'): + if line.strip().startswith('# Name:'): + name_match = line.replace('# Name:', '').strip() + break + + if name_match: + # Check all registries + for comp_type in ['skills', 'commands', 'agents']: + existing = self.check_duplicate(comp_type, name_match) + if existing: + print(f" ⚠️ Found existing {comp_type[:-1]}: {name_match}") + report["skipped_components"].append({ + "type": comp_type[:-1], + "name": name_match, + "reason": "Already exists", + "existing": existing + }) + + if not report["skipped_components"]: + print(" ✅ No duplicates found\n") + else: + print() + + # Step 3: Create components based on type + print("🛠️ Step 3: Creating components...\n") + + # If it's a command, analyze complexity first + if desc_type["is_command"]: + print(" 📊 Analyzing command complexity...") + creator = meta_command.CommandCreator(base_dir=str(self.base_dir)) + + # Read content for analysis + with open(description_path) as f: + full_content = f.read() + + cmd_desc = creator.parse_description(description_path) + analysis = creator.analyze_complexity(cmd_desc, full_content) + + print(f" Recommended pattern: {analysis['recommended_pattern']}") + print(f" Should create skill: {analysis['should_create_skill']}\n") + + # Update desc_type based on analysis + if analysis['should_create_skill']: + desc_type['is_skill'] = True + + # Create skill first if needed + if desc_type["is_skill"]: + print(" 🔧 Creating skill...") + skill_result = self.create_skill(description_path, requirement) + + if skill_result.get("errors"): + report["errors"].extend(skill_result["errors"]) + print(f" ⚠️ Skill creation had warnings\n") + else: + print(f" ✅ Skill '{skill_result['skill_name']}' created\n") + report["created_components"].append({ + "type": "skill", + "name": skill_result["skill_name"], + "files": skill_result.get("created_files", []) + }) + + # Create command if needed + if desc_type["is_command"]: + print(" 📜 Creating command...") + command_result = self.create_command(description_path, requirement) + + if command_result.get("ok"): + print(f" ✅ Command '{command_result['command_name']}' created\n") + report["created_components"].append({ + "type": "command", + "name": command_result["command_name"], + "manifest": command_result.get("manifest_file"), + "pattern": command_result.get("complexity_analysis", {}).get("recommended_pattern") + }) + else: + report["errors"].append(f"Command creation failed: {command_result.get('error')}") + print(f" ❌ Command creation failed\n") + + # Create agent if needed + if desc_type["is_agent"]: + print(" 🤖 Creating agent...") + agent_result = self.create_agent(description_path, requirement) + + print(f" ✅ Agent '{agent_result['name']}' created") + print(f" Skills: {', '.join(agent_result.get('skills', []))}\n") + + report["created_components"].append({ + "type": "agent", + "name": agent_result["name"], + "files": [agent_result.get("agent_yaml"), agent_result.get("readme")], + "skills": agent_result.get("skills", []) + }) + + # Step 4: Validate compatibility for agents + print("🔬 Step 4: Validating compatibility...\n") + compatibility = self.validate_compatibility(agent_result["name"]) + + if "error" not in compatibility: + report["compatibility_analysis"] = compatibility + + # Check for gaps + gaps = compatibility.get("gaps", []) + if gaps: + print(f" ⚠️ Found {len(gaps)} gap(s):") + for gap in gaps: + print(f" • {gap['artifact']}: {gap['issue']}") + report["gaps"].append(gap) + print() + + # Add recommendations + for gap in gaps: + report["recommendations"].append( + f"Create skill to produce '{gap['artifact']}' artifact" + ) + else: + print(" ✅ No compatibility gaps found\n") + + # Show compatible agents + if compatibility.get("can_feed_to"): + print(f" ➡️ Can feed to {len(compatibility['can_feed_to'])} agent(s)") + if compatibility.get("can_receive_from"): + print(f" ⬅️ Can receive from {len(compatibility['can_receive_from'])} agent(s)") + print() + + # Step 5: Auto-fill gaps if requested + if auto_fill_gaps and report["gaps"]: + print("🔧 Step 5: Auto-filling gaps...\n") + for gap in report["gaps"]: + print(f" TODO: Auto-create skill for '{gap['artifact']}'") + # TODO: Implement auto-gap-filling + print() + + # Final summary + print("=" * 80) + print("✨ CREATION SUMMARY") + print("=" * 80) + + if report["created_components"]: + print(f"\n✅ Created {len(report['created_components'])} component(s):") + for comp in report["created_components"]: + print(f" • {comp['type'].upper()}: {comp['name']}") + + if report["skipped_components"]: + print(f"\n⏭️ Skipped {len(report['skipped_components'])} component(s) (already exist):") + for comp in report["skipped_components"]: + print(f" • {comp['type'].upper()}: {comp['name']}") + + if report["gaps"]: + print(f"\n⚠️ Found {len(report['gaps'])} compatibility gap(s)") + + if report["recommendations"]: + print("\n💡 Recommendations:") + for rec in report["recommendations"]: + print(f" • {rec}") + + print("\n" + "=" * 80 + "\n") + + return report + + except Exception as e: + logger.error(f"Error during orchestration: {e}", exc_info=True) + report["ok"] = False + report["errors"].append(str(e)) + print(f"\n❌ Error: {e}\n") + return report + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="meta.create - Intelligent component creation orchestrator" + ) + parser.add_argument( + "description", + help="Path to component description file (.md or .json)" + ) + parser.add_argument( + "--auto-fill-gaps", + action="store_true", + help="Automatically create missing dependencies" + ) + parser.add_argument( + "--skip-duplicate-check", + action="store_true", + help="Skip checking for existing components" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml", "text"], + default="text", + help="Output format for final report" + ) + + # Traceability arguments + parser.add_argument( + "--requirement-id", + help="Requirement identifier (e.g., REQ-2025-001)" + ) + parser.add_argument( + "--requirement-description", + help="What this component accomplishes" + ) + parser.add_argument( + "--requirement-source", + help="Source document" + ) + parser.add_argument( + "--issue-id", + help="Issue tracking ID (e.g., JIRA-123)" + ) + parser.add_argument( + "--requested-by", + help="Who requested this" + ) + parser.add_argument( + "--rationale", + help="Why this is needed" + ) + + args = parser.parse_args() + + # Create requirement info if provided + requirement = None + if args.requirement_id and args.requirement_description: + requirement = RequirementInfo( + id=args.requirement_id, + description=args.requirement_description, + source=args.requirement_source, + issue_id=args.issue_id, + requested_by=args.requested_by, + rationale=args.rationale + ) + + orchestrator = ComponentCreator() + result = orchestrator.orchestrate_creation( + description_path=args.description, + auto_fill_gaps=args.auto_fill_gaps, + check_duplicates=not args.skip_duplicate_check, + requirement=requirement + ) + + # Output final report in requested format + if args.output_format == "json": + print(json.dumps(result, indent=2)) + elif args.output_format == "yaml": + print(yaml.dump(result, default_flow_style=False)) + + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/agents/meta.hook/README.md b/agents/meta.hook/README.md new file mode 100644 index 0000000..90e7d2d --- /dev/null +++ b/agents/meta.hook/README.md @@ -0,0 +1,442 @@ +# meta.hook - Hook Creator Meta-Agent + +Generates Claude Code hooks from natural language descriptions. + +## Overview + +**meta.hook** is a meta-agent that creates Claude Code hooks from simple description files. It generates hook configurations that execute commands in response to events like tool calls, errors, or user interactions. + +**What it does:** +- Parses hook descriptions (Markdown or JSON) +- Generates `.claude/hooks.yaml` configurations +- Validates event types and hook structure +- Manages hook lifecycle (create, update, enable/disable) +- Supports tool-specific filtering + +## Quick Start + +### Create a Hook + +```bash +python3 agents/meta.hook/meta_hook.py examples/my_hook.md +``` + +Output: +``` +🪝 meta.hook - Creating hook from examples/my_hook.md + +✨ Hook 'pre-commit-lint' created successfully! + +📄 Created/updated file: + - .claude/hooks.yaml + +✅ Hook 'pre-commit-lint' is ready to use + Event: before-tool-call + Command: npm run lint +``` + +### Hook Description Format + +Create a Markdown file: + +```markdown +# Name: pre-commit-lint + +# Event: before-tool-call + +# Tool Filter: git + +# Description: Run linter before git commits + +# Command: npm run lint + +# Timeout: 30000 + +# Enabled: true +``` + +Or use JSON format: + +```json +{ + "name": "pre-commit-lint", + "event": "before-tool-call", + "tool_filter": "git", + "description": "Run linter before git commits", + "command": "npm run lint", + "timeout": 30000, + "enabled": true +} +``` + +## Event Types + +Supported Claude Code events: + +- **before-tool-call** - Before any tool is executed +- **after-tool-call** - After any tool completes +- **on-error** - When a tool call fails +- **user-prompt-submit** - When user submits a prompt +- **assistant-response** - After assistant responds + +## Generated Structure + +meta.hook generates or updates `.claude/hooks.yaml`: + +```yaml +hooks: +- name: pre-commit-lint + event: before-tool-call + command: npm run lint + description: Run linter before git commits + enabled: true + tool_filter: git + timeout: 30000 +``` + +## Usage Examples + +### Example 1: Pre-commit Linting + +**Description file** (`lint_hook.md`): + +```markdown +# Name: pre-commit-lint + +# Event: before-tool-call + +# Tool Filter: git + +# Description: Run linter before git commits to ensure code quality + +# Command: npm run lint + +# Timeout: 30000 +``` + +**Create hook:** + +```bash +python3 agents/meta.hook/meta_hook.py lint_hook.md +``` + +### Example 2: Post-deployment Notification + +**Description file** (`deploy_notify.json`): + +```json +{ + "name": "deploy-notify", + "event": "after-tool-call", + "tool_filter": "deploy", + "description": "Send notification after deployment", + "command": "./scripts/notify-team.sh", + "timeout": 10000 +} +``` + +**Create hook:** + +```bash +python3 agents/meta.hook/meta_hook.py deploy_notify.json +``` + +### Example 3: Error Logging + +**Description file** (`error_logger.md`): + +```markdown +# Name: error-logger + +# Event: on-error + +# Description: Log errors to monitoring system + +# Command: ./scripts/log-error.sh "{error}" "{tool}" + +# Timeout: 5000 + +# Enabled: true +``` + +**Create hook:** + +```bash +python3 agents/meta.hook/meta_hook.py error_logger.md +``` + +## Hook Parameters + +### Required + +- **name** - Unique hook identifier +- **event** - Trigger event type +- **command** - Shell command to execute + +### Optional + +- **description** - What the hook does +- **tool_filter** - Only trigger for specific tools (e.g., "git", "npm", "docker") +- **enabled** - Whether hook is active (default: true) +- **timeout** - Command timeout in milliseconds (default: none) + +## Tool Filters + +Restrict hooks to specific tools: + +```markdown +# Tool Filter: git +``` + +This hook only triggers for git-related tool calls. + +Common tool filters: +- `git` - Git operations +- `npm` - NPM commands +- `docker` - Docker commands +- `python` - Python execution +- `bash` - Shell commands + +## Managing Hooks + +### Update Existing Hook + +Run meta.hook with the same hook name to update: + +```bash +python3 agents/meta.hook/meta_hook.py updated_hook.md +``` + +Output: +``` +⚠️ Warning: Hook 'pre-commit-lint' already exists, updating... +✨ Hook 'pre-commit-lint' created successfully! +``` + +### Disable Hook + +Set `Enabled: false` in description: + +```markdown +# Name: my-hook +# Event: before-tool-call +# Command: echo "test" +# Enabled: false +``` + +### Multiple Hooks + +Create multiple hook descriptions and run meta.hook for each: + +```bash +for hook in hooks/*.md; do + python3 agents/meta.hook/meta_hook.py "$hook" +done +``` + +## Integration + +### With Claude Code + +Hooks are automatically loaded by Claude Code from `.claude/hooks.yaml`. + +### With meta.agent + +Create agents that use hooks: + +```yaml +name: ci.agent +description: Continuous integration agent +# Hooks will trigger during agent execution +``` + +## Artifact Types + +### Consumes + +- **hook-description** - Natural language hook requirements + - Pattern: `**/hook_description.md` + - Format: Markdown or JSON + +### Produces + +- **hook-config** - Claude Code hook configuration + - Pattern: `.claude/hooks.yaml` + - Schema: `schemas/hook-config.json` + +## Common Workflows + +### Workflow 1: Create and Test Hook + +```bash +# 1. Create hook description +cat > my_hook.md < lint_hook.md < test_hook.md < error_notify.md < + +Examples: + python3 agents/meta.hook/meta_hook.py examples/lint_hook.md + python3 agents/meta.hook/meta_hook.py examples/notify_hook.json +""" + +import os +import sys +import json +import yaml +import re +from pathlib import Path +from typing import Dict, List, Any, Optional + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.traceability import get_tracer, RequirementInfo + +logger = setup_logger(__name__) + + +class HookCreator: + """Creates Claude Code hooks from descriptions""" + + VALID_EVENTS = [ + "before-tool-call", + "after-tool-call", + "on-error", + "user-prompt-submit", + "assistant-response" + ] + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize hook creator""" + self.base_dir = Path(base_dir) + self.hooks_dir = self.base_dir / ".claude" + + def parse_description(self, description_path: str) -> Dict[str, Any]: + """ + Parse hook description from Markdown or JSON file + + Args: + description_path: Path to description file + + Returns: + Dict with hook configuration + """ + path = Path(description_path) + + if not path.exists(): + raise FileNotFoundError(f"Description file not found: {description_path}") + + # Read file + content = path.read_text() + + # Try JSON first + if path.suffix == ".json": + return json.loads(content) + + # Parse Markdown format + hook_desc = {} + + # Extract fields + patterns = { + "name": r"#\s*Name:\s*(.+)", + "event": r"#\s*Event:\s*(.+)", + "description": r"#\s*Description:\s*(.+)", + "command": r"#\s*Command:\s*(.+)", + "tool_filter": r"#\s*Tool\s*Filter:\s*(.+)", + "enabled": r"#\s*Enabled:\s*(.+)", + "timeout": r"#\s*Timeout:\s*(\d+)" + } + + for field, pattern in patterns.items(): + match = re.search(pattern, content, re.IGNORECASE) + if match: + value = match.group(1).strip() + + # Convert types + if field == "enabled": + value = value.lower() in ("true", "yes", "1") + elif field == "timeout": + value = int(value) + + hook_desc[field] = value + + # Validate required fields + required = ["name", "event", "command"] + missing = [f for f in required if f not in hook_desc] + if missing: + raise ValueError(f"Missing required fields: {', '.join(missing)}") + + # Validate event type + if hook_desc["event"] not in self.VALID_EVENTS: + raise ValueError( + f"Invalid event type: {hook_desc['event']}. " + f"Must be one of: {', '.join(self.VALID_EVENTS)}" + ) + + return hook_desc + + def generate_hooks_yaml(self, hook_desc: Dict[str, Any]) -> str: + """ + Generate hooks.yaml configuration + + Args: + hook_desc: Parsed hook description + + Returns: + YAML string + """ + hook_config = { + "name": hook_desc["name"], + "event": hook_desc["event"], + "command": hook_desc["command"] + } + + # Add optional fields + if "description" in hook_desc: + hook_config["description"] = hook_desc["description"] + + if "enabled" in hook_desc: + hook_config["enabled"] = hook_desc["enabled"] + else: + hook_config["enabled"] = True + + if "tool_filter" in hook_desc: + hook_config["tool_filter"] = hook_desc["tool_filter"] + + if "timeout" in hook_desc: + hook_config["timeout"] = hook_desc["timeout"] + + # Wrap in hooks array + hooks_yaml = {"hooks": [hook_config]} + + return yaml.dump(hooks_yaml, default_flow_style=False, sort_keys=False) + + def create_hook( + self, + description_path: str, + requirement: Optional[RequirementInfo] = None + ) -> Dict[str, Any]: + """ + Create hook from description file + + Args: + description_path: Path to description file + requirement: Optional requirement information for traceability + + Returns: + Dict with creation results + """ + try: + print(f"🪝 meta.hook - Creating hook from {description_path}\n") + + # Parse description + hook_desc = self.parse_description(description_path) + + # Generate hooks.yaml + hooks_yaml = self.generate_hooks_yaml(hook_desc) + + # Ensure .claude directory exists + self.hooks_dir.mkdir(parents=True, exist_ok=True) + + # Write hooks.yaml (or append if exists) + hooks_file = self.hooks_dir / "hooks.yaml" + + if hooks_file.exists(): + # Load existing hooks + existing = yaml.safe_load(hooks_file.read_text()) + if not existing or not isinstance(existing, dict): + existing = {"hooks": []} + if "hooks" not in existing: + existing["hooks"] = [] + if not isinstance(existing["hooks"], list): + existing["hooks"] = [] + + # Add new hook + new_hook = yaml.safe_load(hooks_yaml)["hooks"][0] + + # Check for duplicate + hook_names = [h.get("name") for h in existing["hooks"] if isinstance(h, dict)] + if new_hook["name"] in hook_names: + print(f"⚠️ Warning: Hook '{new_hook['name']}' already exists, updating...") + # Remove old version + existing["hooks"] = [h for h in existing["hooks"] if h["name"] != new_hook["name"]] + + existing["hooks"].append(new_hook) + hooks_yaml = yaml.dump(existing, default_flow_style=False, sort_keys=False) + + # Write file + hooks_file.write_text(hooks_yaml) + + print(f"✨ Hook '{hook_desc['name']}' created successfully!\n") + print(f"📄 Created/updated file:") + print(f" - {hooks_file}\n") + print(f"✅ Hook '{hook_desc['name']}' is ready to use") + print(f" Event: {hook_desc['event']}") + print(f" Command: {hook_desc['command']}") + + result = { + "ok": True, + "status": "success", + "hook_name": hook_desc["name"], + "hooks_file": str(hooks_file) + } + + # Log traceability if requirement provided + trace_id = None + if requirement: + try: + tracer = get_tracer() + + # Create component ID from hook name + component_id = f"hook.{hook_desc['name'].replace('-', '_')}" + + trace_id = tracer.log_creation( + component_id=component_id, + component_name=hook_desc["name"], + component_type="hook", + component_version="0.1.0", + component_file_path=str(hooks_file), + input_source_path=description_path, + created_by_tool="meta.hook", + created_by_version="0.1.0", + requirement=requirement, + tags=["hook", "auto-generated", hook_desc["event"]], + project="Betty Framework" + ) + + # Log validation check + validation_details = { + "checks_performed": [ + {"name": "hook_structure", "status": "passed"}, + {"name": "event_validation", "status": "passed", + "message": f"Valid event type: {hook_desc['event']}"} + ] + } + + # Check for tool filter + if hook_desc.get("tool_filter"): + validation_details["checks_performed"].append({ + "name": "tool_filter_validation", + "status": "passed", + "message": f"Tool filter: {hook_desc['tool_filter']}" + }) + + tracer.log_verification( + component_id=component_id, + check_type="validation", + tool="meta.hook", + result="passed", + details=validation_details + ) + + result["trace_id"] = trace_id + result["component_id"] = component_id + + except Exception as e: + print(f"⚠️ Warning: Could not log traceability: {e}") + + return result + + except Exception as e: + print(f"❌ Error creating hook: {e}") + logger.error(f"Error creating hook: {e}", exc_info=True) + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="meta.hook - Create hooks from descriptions" + ) + parser.add_argument( + "description", + help="Path to hook description file (.md or .json)" + ) + + # Traceability arguments + parser.add_argument( + "--requirement-id", + help="Requirement identifier (e.g., REQ-2025-001)" + ) + parser.add_argument( + "--requirement-description", + help="What this hook accomplishes" + ) + parser.add_argument( + "--requirement-source", + help="Source document" + ) + parser.add_argument( + "--issue-id", + help="Issue tracking ID (e.g., JIRA-123)" + ) + parser.add_argument( + "--requested-by", + help="Who requested this" + ) + parser.add_argument( + "--rationale", + help="Why this is needed" + ) + + args = parser.parse_args() + + # Create requirement info if provided + requirement = None + if args.requirement_id and args.requirement_description: + requirement = RequirementInfo( + id=args.requirement_id, + description=args.requirement_description, + source=args.requirement_source, + issue_id=args.issue_id, + requested_by=args.requested_by, + rationale=args.rationale + ) + + creator = HookCreator() + result = creator.create_hook(args.description, requirement=requirement) + + # Display traceability info if available + if result.get("trace_id"): + print(f"\n📝 Traceability: {result['trace_id']}") + print(f" View trace: python3 betty/trace_cli.py show {result['component_id']}") + + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/agents/meta.skill/README.md b/agents/meta.skill/README.md new file mode 100644 index 0000000..09bb06d --- /dev/null +++ b/agents/meta.skill/README.md @@ -0,0 +1,647 @@ +# meta.skill - Skill Creator Meta-Agent + +Generates complete Betty skills from natural language descriptions. + +## Overview + +**meta.skill** is a meta-agent that creates fully functional skills from simple description files. It generates skill definitions, Python implementations, tests, and documentation, following Betty Framework conventions. + +**What it does:** +- Parses skill descriptions (Markdown or JSON) +- Generates `skill.yaml` configurations +- Creates Python implementation stubs +- Generates test templates +- Creates comprehensive README documentation +- Validates skill names and structure +- Registers artifact metadata + +## Quick Start + +### Create a Skill + +```bash +# Create skill from description +python3 agents/meta.skill/meta_skill.py examples/my_skill_description.md +``` + +Output: +``` +🛠️ meta.skill - Creating skill from examples/my_skill_description.md + +✨ Skill 'data.transform' created successfully! + +📄 Created files: + - skills/data.transform/skill.yaml + - skills/data.transform/data_transform.py + - skills/data.transform/test_data_transform.py + - skills/data.transform/README.md + +✅ Skill 'data.transform' is ready to use + Add to agent skills_available to use it. +``` + +### Skill Description Format + +Create a Markdown file with this structure: + +```markdown +# Name: domain.action + +# Purpose: +Brief description of what the skill does + +# Inputs: +- input_parameter_1 +- input_parameter_2 (optional) + +# Outputs: +- output_file_1.json +- output_file_2.yaml + +# Permissions: +- filesystem:read +- filesystem:write + +# Produces Artifacts: +- artifact-type-1 +- artifact-type-2 + +# Consumes Artifacts: +- artifact-type-3 + +# Implementation Notes: +Detailed guidance for implementing the skill logic +``` + +Or use JSON format: + +```json +{ + "name": "domain.action", + "purpose": "Brief description", + "inputs": ["param1", "param2"], + "outputs": ["output.json"], + "permissions": ["filesystem:read"], + "artifact_produces": ["artifact-type-1"], + "artifact_consumes": ["artifact-type-2"], + "implementation_notes": "Implementation guidance" +} +``` + +## Generated Structure + +For a skill named `data.transform`, meta.skill generates: + +``` +skills/data.transform/ +├── skill.yaml # Skill configuration +├── data_transform.py # Python implementation +├── test_data_transform.py # Test suite +└── README.md # Documentation +``` + +### skill.yaml + +Complete skill configuration following Betty conventions: + +```yaml +name: data.transform +version: 0.1.0 +description: Transform data between formats +inputs: + - input_file + - output_format +outputs: + - transformed_data.json +status: active +permissions: + - filesystem:read + - filesystem:write +entrypoints: + - command: /data/transform + handler: data_transform.py + runtime: python + description: Transform data between formats +artifact_metadata: + produces: + - type: transformed-data + consumes: + - type: raw-data +``` + +### Implementation Stub + +Python implementation with: +- Proper imports and logging +- Class structure +- execute() method with typed parameters +- CLI entry point with argparse +- Error handling +- Output formatting (JSON/YAML) + +```python +#!/usr/bin/env python3 +""" +data.transform - Transform data between formats + +Generated by meta.skill +""" + +import os +import sys +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +class DataTransform: + """Transform data between formats""" + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + def execute(self, input_file: Optional[str] = None, + output_format: Optional[str] = None) -> Dict[str, Any]: + """Execute the skill""" + try: + logger.info("Executing data.transform...") + + # TODO: Implement skill logic here + # Implementation notes: [your notes here] + + result = { + "ok": True, + "status": "success", + "message": "Skill executed successfully" + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Transform data between formats" + ) + + parser.add_argument("--input-file", help="input_file") + parser.add_argument("--output-format", help="output_format") + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + skill = DataTransform() + result = skill.execute( + input_file=args.input_file, + output_format=args.output_format, + ) + + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() +``` + +### Test Template + +pytest-based test suite: + +```python +#!/usr/bin/env python3 +"""Tests for data.transform""" + +import pytest +import sys +import os +from pathlib import Path + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.data_transform import data_transform + + +class TestDataTransform: + """Tests for DataTransform""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = data_transform.DataTransform() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["data_transform.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + data_transform.main() + + assert exc_info.value.code == 0 +``` + +## Skill Naming Convention + +Skills must follow the `domain.action` format: +- **domain**: Category (e.g., `data`, `api`, `file`, `text`) +- **action**: Operation (e.g., `validate`, `transform`, `parse`) +- Use only lowercase letters and numbers (no hyphens, underscores, or special characters) + +Valid examples: +- ✅ `data.validate` +- ✅ `api.test` +- ✅ `file.compress` +- ✅ `text.summarize` + +Invalid examples: +- ❌ `data.validate-json` (hyphen not allowed) +- ❌ `data_validate` (underscore not allowed) +- ❌ `DataValidate` (uppercase not allowed) +- ❌ `validate` (missing domain) + +## Usage Examples + +### Example 1: JSON Validator + +**Description file** (`json_validator.md`): + +```markdown +# Name: data.validatejson + +# Purpose: +Validates JSON files against JSON Schema definitions + +# Inputs: +- json_file_path +- schema_file_path (optional) + +# Outputs: +- validation_result.json + +# Permissions: +- filesystem:read + +# Produces Artifacts: +- validation-report + +# Implementation Notes: +Use Python's jsonschema library for validation +``` + +**Create skill:** + +```bash +python3 agents/meta.skill/meta_skill.py json_validator.md +``` + +### Example 2: API Tester + +**Description file** (`api_tester.json`): + +```json +{ + "name": "api.test", + "purpose": "Test API endpoints and generate reports", + "inputs": ["openapi_spec_path", "base_url"], + "outputs": ["test_results.json"], + "permissions": ["network:http"], + "artifact_produces": ["test-report"], + "artifact_consumes": ["openapi-spec"], + "implementation_notes": "Use requests library to test each endpoint" +} +``` + +**Create skill:** + +```bash +python3 agents/meta.skill/meta_skill.py api_tester.json +``` + +### Example 3: File Compressor + +**Description file** (`file_compressor.md`): + +```markdown +# Name: file.compress + +# Purpose: +Compress files using various algorithms + +# Inputs: +- input_path +- compression_type (gzip, zip, tar.gz) + +# Outputs: +- compressed_file + +# Permissions: +- filesystem:read +- filesystem:write + +# Implementation Notes: +Support gzip, zip, and tar.gz formats using Python standard library +``` + +**Create skill:** + +```bash +python3 agents/meta.skill/meta_skill.py file_compressor.md +``` + +## Integration + +### With meta.agent + +Create an agent that uses the skill: + +```yaml +name: data.validator +description: Data validation agent +skills_available: + - data.validatejson # Skill created by meta.skill +``` + +### With plugin.sync + +Sync skills to plugin format: + +```bash +python3 skills/plugin.sync/plugin_sync.py +``` + +This converts `skill.yaml` to commands in `.claude-plugin/plugin.yaml`. + +## Artifact Types + +### Consumes + +- **skill-description** - Natural language skill requirements + - Pattern: `**/skill_description.md` + - Format: Markdown or JSON + +### Produces + +- **skill-definition** - Complete skill configuration + - Pattern: `skills/*/skill.yaml` + - Schema: `schemas/skill-definition.json` + +- **skill-implementation** - Python implementation code + - Pattern: `skills/*/[skill_module].py` + +- **skill-tests** - Test suite + - Pattern: `skills/*/test_[skill_module].py` + +- **skill-documentation** - README documentation + - Pattern: `skills/*/README.md` + +## Common Workflows + +### Workflow 1: Create and Test Skill + +```bash +# 1. Create skill description +cat > my_skill.md <> agents/api.agent/agent.yaml + +# 3. Sync to plugin +python3 skills/plugin.sync/plugin_sync.py +``` + +### Workflow 3: Batch Create Skills + +```bash +# Create multiple skills +for desc in skills_to_create/*.md; do + echo "Creating skill from $desc..." + python3 agents/meta.skill/meta_skill.py "$desc" +done +``` + +## Tips & Best Practices + +### Skill Descriptions + +**Be specific about purpose:** +```markdown +# Good +# Purpose: Validate JSON against JSON Schema Draft 07 + +# Bad +# Purpose: Validate stuff +``` + +**Include implementation notes:** +```markdown +# Implementation Notes: +Use the jsonschema library. Support Draft 07 schemas. +Provide detailed error messages with line numbers. +``` + +**Specify optional parameters:** +```markdown +# Inputs: +- required_param +- optional_param (optional) +- another_optional (optional, defaults to 'value') +``` + +### Parameter Naming + +Parameters are automatically sanitized: +- Special characters removed (except `-`, `_`, spaces) +- Converted to lowercase +- Spaces and hyphens become underscores + +Example conversions: +- `"Schema File Path (optional)"` → `schema_file_path_optional` +- `"API-Key"` → `api_key` +- `"Input Data"` → `input_data` + +### Implementation Strategy + +1. **Generate skeleton first** - Let meta.skill create structure +2. **Implement gradually** - Add logic to `execute()` method +3. **Test incrementally** - Run tests after each change +4. **Update documentation** - Keep README current + +### Artifact Metadata + +Always specify artifact types for interoperability: + +```markdown +# Produces Artifacts: +- openapi-spec +- validation-report + +# Consumes Artifacts: +- api-requirements +``` + +This enables: +- Agent discovery via meta.compatibility +- Pipeline suggestions via meta.suggest +- Workflow orchestration + +## Troubleshooting + +### Invalid skill name + +``` +Error: Skill name must be in domain.action format: my-skill +``` + +**Solution:** Use format `domain.action` with only alphanumeric characters: +```markdown +# Wrong: my-skill, my_skill, MySkill +# Right: data.transform, api.validate +``` + +### Skill already exists + +``` +Error: Skill directory already exists: skills/data.validate +``` + +**Solution:** Remove existing skill or use different name: +```bash +rm -rf skills/data.validate +``` + +### Import errors in generated code + +``` +ModuleNotFoundError: No module named 'betty.config' +``` + +**Solution:** Ensure Betty framework is in Python path: +```bash +export PYTHONPATH="${PYTHONPATH}:/home/user/betty" +``` + +### Test failures + +``` +ModuleNotFoundError: No module named 'skills.data_validate' +``` + +**Solution:** Run tests from Betty root directory: +```bash +cd /home/user/betty +pytest skills/data.validate/test_data_validate.py -v +``` + +## Architecture + +``` +meta.skill + ├─ Input: skill-description (Markdown/JSON) + ├─ Parser: extract name, purpose, inputs, outputs + ├─ Generator: create skill.yaml, Python, tests, README + ├─ Validator: check naming conventions + └─ Output: Complete skill directory structure +``` + +## Next Steps + +After creating a skill with meta.skill: + +1. **Implement logic** - Add functionality to `execute()` method +2. **Write tests** - Expand test coverage beyond basic tests +3. **Add to agent** - Include in agent's `skills_available` +4. **Sync to plugin** - Run plugin.sync to update plugin.yaml +5. **Test integration** - Verify skill works in agent context +6. **Document usage** - Update README with examples + +## Related Documentation + +- [META_AGENTS.md](../../docs/META_AGENTS.md) - Meta-agent ecosystem +- [ARTIFACT_STANDARDS.md](../../docs/ARTIFACT_STANDARDS.md) - Artifact system +- [skill-description schema](../../schemas/skill-description.json) +- [skill-definition schema](../../schemas/skill-definition.json) + +## How Claude Uses This + +Claude can: +1. **Create skills on demand** - "Create a skill that validates YAML files" +2. **Extend agent capabilities** - "Add a JSON validator skill to this agent" +3. **Build skill libraries** - "Create skills for all common data operations" +4. **Prototype quickly** - Test ideas by generating skill scaffolds + +meta.skill enables rapid skill development and agent expansion! diff --git a/agents/meta.skill/agent.yaml b/agents/meta.skill/agent.yaml new file mode 100644 index 0000000..53b2452 --- /dev/null +++ b/agents/meta.skill/agent.yaml @@ -0,0 +1,306 @@ +name: meta.skill +version: 0.4.0 +description: | + Creates complete, functional skills from natural language descriptions. + + This meta-agent transforms skill descriptions into production-ready skills with: + - Complete skill.yaml definition with validated artifact types + - Artifact flow analysis showing producers/consumers + - Production-quality Python implementation with type hints + - Comprehensive test templates + - Complete documentation with examples + - Dependency validation + - Registry registration with artifact_metadata + - Discoverability verification + + Ensures skills follow Betty Framework conventions and are ready for use in agents. + + Version 0.4.0 adds artifact flow analysis, improved code templates with + type hints parsed from skill.yaml, and dependency validation. + +artifact_metadata: + consumes: + - type: skill-description + file_pattern: "**/skill_description.md" + content_type: "text/markdown" + description: "Natural language description of skill requirements" + schema: "schemas/skill-description.json" + + produces: + - type: skill-definition + file_pattern: "skills/*/skill.yaml" + content_type: "application/yaml" + schema: "schemas/skill-definition.json" + description: "Complete skill configuration" + + - type: skill-implementation + file_pattern: "skills/*/*.py" + content_type: "text/x-python" + description: "Python implementation with proper structure" + + - type: skill-tests + file_pattern: "skills/*/test_*.py" + content_type: "text/x-python" + description: "Test template with example tests" + + - type: skill-documentation + file_pattern: "skills/*/SKILL.md" + content_type: "text/markdown" + description: "Skill documentation and usage guide" + +status: draft +reasoning_mode: iterative +capabilities: + - Convert skill concepts into production-ready packages with tests and docs + - Ensure generated skills follow registry, artifact, and permission conventions + - Coordinate registration and documentation updates for new skills +skills_available: + - skill.create + - skill.define + - artifact.define # Generate artifact metadata + - artifact.validate.types # Validate artifact types against registry + +permissions: + - filesystem:read + - filesystem:write + +system_prompt: | + You are meta.skill, the skill creator for Betty Framework. + + Your purpose is to transform natural language skill descriptions into complete, + production-ready skills that follow Betty conventions. + + ## Your Workflow + + 1. **Parse Description** - Understand skill requirements + - Extract name, purpose, inputs, outputs + - Identify artifact types in produces/consumes sections + - Identify required permissions + - Understand implementation requirements + + 2. **Validate Artifact Types** - CRITICAL: Verify before generating skill.yaml + - Extract ALL artifact types from skill description (produces + consumes sections) + - Call artifact.validate.types skill: + ```bash + python3 skills/artifact.validate.types/artifact_validate_types.py \ + --artifact_types '["threat-model", "data-flow-diagrams", "architecture-overview"]' \ + --check_schemas true \ + --suggest_alternatives true \ + --max_suggestions 3 + ``` + - Parse validation results: + ```json + { + "all_valid": true/false, + "validation_results": { + "threat-model": { + "valid": true, + "file_pattern": "*.threat-model.yaml", + "content_type": "application/yaml", + "schema": "schemas/artifacts/threat-model-schema.json" + } + }, + "invalid_types": ["data-flow-diagram"], + "suggestions": { + "data-flow-diagram": [ + {"type": "data-flow-diagrams", "reason": "Plural form", "confidence": "high"} + ] + } + } + ``` + - If all_valid == false: + → Display invalid_types and suggestions to user + → Example: "❌ Artifact type 'data-flow-diagram' not found. Did you mean 'data-flow-diagrams' (plural, high confidence)?" + → ASK USER to confirm correct types or provide alternatives + → HALT skill creation until artifact types are validated + - If all_valid == true: + → Store validated metadata (file_pattern, content_type, schema) for each type + → Use this exact metadata in Step 3 when generating skill.yaml + + 3. **Analyze Artifact Flow** - Understand skill's place in ecosystem + - For each artifact type the skill produces: + → Search registry for skills that consume this type + → Report: "✅ {artifact_type} will be consumed by: {consuming_skills}" + → If no consumers: "⚠️ {artifact_type} has no consumers yet - consider creating skills that use it" + - For each artifact type the skill consumes: + → Search registry for skills that produce this type + → Report: "✅ {artifact_type} produced by: {producing_skills}" + → If no producers: "❌ {artifact_type} has no producers - user must provide manually or create producer skill first" + - Warn about gaps in artifact flow + - Suggest related skills to create for complete workflow + + 4. **Generate skill.yaml** - Create complete definition with VALIDATED artifact metadata + - name: Proper naming (domain.action format) + - version: Semantic versioning (e.g., "0.1.0") + - description: Clear description of what the skill does + - inputs: List of input parameters (use empty list [] if none) + - outputs: List of output parameters (use empty list [] if none) + - status: One of "draft", "active", or "deprecated" + - Artifact metadata (produces/consumes) + - Permissions + - Entrypoints with parameters + + 5. **Generate Implementation** - Create production-quality Python stub + - **Parse skill.yaml inputs** to generate proper argparse CLI: + ```python + # For each input in skill.yaml: + parser.add_argument( + '--{input.name}', + type={map_type(input.type)}, # string→str, number→int, boolean→bool, array→list + required={input.required}, + default={input.default if not required}, + help="{input.description}" + ) + ``` + - **Generate function signature** with type hints from inputs/outputs: + ```python + def validate_artifact_types( + artifact_types: List[str], + check_schemas: bool = True, + suggest_alternatives: bool = True + ) -> Dict[str, Any]: + \"\"\" + {skill.description} + + Args: + artifact_types: {input.description from skill.yaml} + check_schemas: {input.description from skill.yaml} + ... + + Returns: + {output descriptions from skill.yaml} + \"\"\" + ``` + - **Include implementation pattern** based on skill type: + - Validation skills: load data → validate → return results + - Generator skills: gather inputs → process → save output + - Transform skills: load input → transform → save output + - **Add comprehensive error handling**: + ```python + except FileNotFoundError as e: + logger.error(str(e)) + print(json.dumps({"ok": False, "error": str(e)}, indent=2)) + sys.exit(1) + ``` + - **JSON output structure** matching skill.yaml outputs: + ```python + result = { + "{output1.name}": value1, # From skill.yaml outputs + "{output2.name}": value2, + "ok": True, + "status": "success" + } + print(json.dumps(result, indent=2)) + ``` + - Add proper logging setup + - Include module docstring with usage example + + 6. **Generate Tests** - Create test template + - Unit test structure + - Example test cases + - Fixtures + - Assertions + + 7. **Generate Documentation** - Create SKILL.md + - Purpose and usage + - Input/output examples + - Integration with agents + - Artifact flow (from Step 3 analysis) + - Must include markdown header starting with # + + 8. **Validate Dependencies** - Check Python packages + - For each dependency in skill.yaml: + → Verify package exists on PyPI (if possible) + → Check for known naming issues (e.g., "yaml" vs "pyyaml") + → Warn about version conflicts with existing skills + - Suggest installation command: `pip install {dependencies}` + - If dependencies missing, warn but don't block + + 9. **Register Skill** - Update registry + - Call registry.update with skill manifest path + - Verify skill appears in registry with artifact_metadata + - Confirm skill is discoverable via artifact types + + 10. **Verify Discoverability** - Final validation + - Check skill exists in registry/skills.json + - Verify artifact_metadata is complete + - Test that agent.compose can discover skill by artifact type + - Confirm artifact flow is complete (from Step 3) + + ## Conventions + + **Naming:** + - Skills: `domain.action` (e.g., `api.validate`, `workflow.compose`) + - Use lowercase with dots + - Action should be imperative verb + + **Structure:** + ``` + skills/domain.action/ + ├── skill.yaml (definition) + ├── domain_action.py (implementation) + ├── test_domain_action.py (tests) + └── SKILL.md (docs) + ``` + + **Artifact Metadata:** + - Always define what the skill produces/consumes + - Use registered artifact types from meta.artifact + - Include schemas when applicable + + **Implementation:** + - Follow Python best practices + - Include proper error handling + - Add logging + - CLI with argparse + - JSON output for results + + ## Quality Standards + + - ✅ Follows Betty conventions (domain.action naming, proper structure) + - ✅ All required fields in skill.yaml: name, version, description, inputs, outputs, status + - ✅ Artifact types VALIDATED against registry before generation + - ✅ Artifact flow ANALYZED (producers/consumers identified) + - ✅ Production-quality code with type hints and comprehensive docstrings + - ✅ Proper CLI generated from skill.yaml inputs (no TODO placeholders) + - ✅ JSON output structure matches skill.yaml outputs + - ✅ Dependencies VALIDATED and installation command provided + - ✅ Comprehensive test template with fixtures + - ✅ SKILL.md with markdown header, examples, and artifact flow + - ✅ Registered in registry with complete artifact_metadata + - ✅ Passes Pydantic validation + - ✅ Discoverable via agent.compose by artifact type + + ## Error Handling & Recovery + + **Artifact Type Not Found:** + - Search registry/artifact_types.json for similar names + - Check for singular/plural variants (data-model vs logical-data-model) + - Suggest alternatives: "Did you mean: 'data-flow-diagrams', 'dataflow-diagram'?" + - ASK USER to confirm or provide correct type + - DO NOT proceed with invalid artifact types + + **File Pattern Mismatch:** + - Use exact file_pattern from registry + - Warn user if description specifies different pattern + - Document correct pattern in skill.yaml comments + + **Schema File Missing:** + - Warn: "Schema file schemas/artifacts/X-schema.json not found" + - Ask if schema should be: (a) created, (b) omitted, (c) ignored + - Continue with warning but don't block skill creation + + **Registry Update Fails:** + - Report specific error from registry.update + - Check if it's version conflict or validation issue + - Provide manual registration command as fallback + - Log issue for framework team + + **Duplicate Skill Name:** + - Check existing version in registry + - Offer to: (a) version bump, (b) rename skill, (c) cancel + - Require explicit user confirmation before overwriting + + Remember: You're creating building blocks for agents. Make skills + composable, well-documented, and easy to use. ALWAYS validate artifact + types before generating skill.yaml! diff --git a/agents/meta.skill/meta_skill.py b/agents/meta.skill/meta_skill.py new file mode 100755 index 0000000..c49ed2b --- /dev/null +++ b/agents/meta.skill/meta_skill.py @@ -0,0 +1,791 @@ +#!/usr/bin/env python3 +""" +meta.skill - Skill Creator + +Creates complete, functional skills from natural language descriptions. +Generates skill.yaml, implementation stub, tests, and documentation. +""" + +import json +import yaml +import sys +import os +import re +from pathlib import Path +from typing import Dict, List, Any, Optional +from datetime import datetime + +# Add parent directory to path for imports +parent_dir = str(Path(__file__).parent.parent.parent) +sys.path.insert(0, parent_dir) + +from betty.traceability import get_tracer, RequirementInfo + +# Import artifact validation from artifact.define skill +try: + import importlib.util + artifact_define_path = Path(__file__).parent.parent.parent / "skills" / "artifact.define" / "artifact_define.py" + spec = importlib.util.spec_from_file_location("artifact_define", artifact_define_path) + artifact_define_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(artifact_define_module) + + validate_artifact_type = artifact_define_module.validate_artifact_type + KNOWN_ARTIFACT_TYPES = artifact_define_module.KNOWN_ARTIFACT_TYPES + ARTIFACT_VALIDATION_AVAILABLE = True +except Exception as e: + ARTIFACT_VALIDATION_AVAILABLE = False + + +class SkillCreator: + """Creates skills from natural language descriptions""" + + def __init__(self, base_dir: str = "."): + """Initialize with base directory""" + self.base_dir = Path(base_dir) + self.skills_dir = self.base_dir / "skills" + self.registry_path = self.base_dir / "registry" / "skills.json" + + def parse_description(self, description_path: str) -> Dict[str, Any]: + """ + Parse skill description from Markdown or JSON file + + Args: + description_path: Path to skill_description.md or .json + + Returns: + Parsed description with skill metadata + """ + path = Path(description_path) + + if not path.exists(): + raise FileNotFoundError(f"Description not found: {description_path}") + + # Handle JSON format + if path.suffix == ".json": + with open(path) as f: + return json.load(f) + + # Handle Markdown format + with open(path) as f: + content = f.read() + + # Parse Markdown sections + description = { + "name": "", + "purpose": "", + "inputs": [], + "outputs": [], + "permissions": [], + "implementation_notes": "", + "examples": [], + "artifact_produces": [], + "artifact_consumes": [] + } + + current_section = None + for line in content.split('\n'): + line_stripped = line.strip() + + # Section headers + if line_stripped.startswith('# Name:'): + description["name"] = line_stripped.replace('# Name:', '').strip() + elif line_stripped.startswith('# Purpose:'): + current_section = "purpose" + elif line_stripped.startswith('# Inputs:'): + current_section = "inputs" + elif line_stripped.startswith('# Outputs:'): + current_section = "outputs" + elif line_stripped.startswith('# Permissions:'): + current_section = "permissions" + elif line_stripped.startswith('# Implementation Notes:'): + current_section = "implementation_notes" + elif line_stripped.startswith('# Examples:'): + current_section = "examples" + elif line_stripped.startswith('# Produces Artifacts:'): + current_section = "artifact_produces" + elif line_stripped.startswith('# Consumes Artifacts:'): + current_section = "artifact_consumes" + elif line_stripped and not line_stripped.startswith('#'): + # Content for current section + if current_section == "purpose": + description["purpose"] += line_stripped + " " + elif current_section == "implementation_notes": + description["implementation_notes"] += line_stripped + " " + elif current_section in ["inputs", "outputs", "permissions", + "examples", "artifact_produces", + "artifact_consumes"] and line_stripped.startswith('-'): + description[current_section].append(line_stripped[1:].strip()) + + description["purpose"] = description["purpose"].strip() + description["implementation_notes"] = description["implementation_notes"].strip() + + return description + + def generate_skill_yaml(self, skill_desc: Dict[str, Any]) -> str: + """ + Generate skill.yaml content + + Args: + skill_desc: Parsed skill description + + Returns: + YAML content as string + """ + skill_name = skill_desc["name"] + + # Convert skill.name to skill_name format for handler + handler_name = skill_name.replace('.', '_') + ".py" + + skill_def = { + "name": skill_name, + "version": "0.1.0", + "description": skill_desc["purpose"], + "inputs": skill_desc.get("inputs", []), + "outputs": skill_desc.get("outputs", []), + "status": "active", + "permissions": skill_desc.get("permissions", ["filesystem:read"]), + "entrypoints": [ + { + "command": f"/{skill_name.replace('.', '/')}", + "handler": handler_name, + "runtime": "python", + "description": skill_desc["purpose"][:100] + } + ] + } + + # Add artifact metadata if specified + if skill_desc.get("artifact_produces") or skill_desc.get("artifact_consumes"): + artifact_metadata = {} + + if skill_desc.get("artifact_produces"): + artifact_metadata["produces"] = [ + {"type": art_type} for art_type in skill_desc["artifact_produces"] + ] + + if skill_desc.get("artifact_consumes"): + artifact_metadata["consumes"] = [ + {"type": art_type, "required": True} + for art_type in skill_desc["artifact_consumes"] + ] + + skill_def["artifact_metadata"] = artifact_metadata + + return yaml.dump(skill_def, default_flow_style=False, sort_keys=False) + + def generate_implementation(self, skill_desc: Dict[str, Any]) -> str: + """ + Generate Python implementation stub + + Args: + skill_desc: Parsed skill description + + Returns: + Python code as string + """ + skill_name = skill_desc["name"] + module_name = skill_name.replace('.', '_') + class_name = ''.join(word.capitalize() for word in skill_name.split('.')) + + implementation = f'''#!/usr/bin/env python3 +""" +{skill_name} - {skill_desc["purpose"]} + +Generated by meta.skill with Betty Framework certification +""" + +import os +import sys +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional + +# Add parent directory to path for imports +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.certification import certified_skill + +logger = setup_logger(__name__) + + +class {class_name}: + """ + {skill_desc["purpose"]} + """ + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + @certified_skill("{skill_name}") + def execute(self''' + + # Add input parameters + if skill_desc.get("inputs"): + for inp in skill_desc["inputs"]: + # Sanitize parameter names - remove special characters, keep only alphanumeric and underscores + param_name = ''.join(c if c.isalnum() or c in ' -_' else '' for c in inp.lower()) + param_name = param_name.replace(' ', '_').replace('-', '_') + implementation += f', {param_name}: Optional[str] = None' + + implementation += f''') -> Dict[str, Any]: + """ + Execute the skill + + Returns: + Dict with execution results + """ + try: + logger.info("Executing {skill_name}...") + + # TODO: Implement skill logic here +''' + + if skill_desc.get("implementation_notes"): + implementation += f''' + # Implementation notes: + # {skill_desc["implementation_notes"]} +''' + + # Escape the purpose string for Python string literal + escaped_purpose = skill_desc['purpose'].replace('"', '\\"') + + implementation += f''' + # Placeholder implementation + result = {{ + "ok": True, + "status": "success", + "message": "Skill executed successfully" + }} + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {{e}}") + return {{ + "ok": False, + "status": "failed", + "error": str(e) + }} + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="{escaped_purpose}" + ) +''' + + # Add CLI arguments for inputs + if skill_desc.get("inputs"): + for inp in skill_desc["inputs"]: + # Sanitize parameter names - remove special characters + param_name = ''.join(c if c.isalnum() or c in ' -_' else '' for c in inp.lower()) + param_name = param_name.replace(' ', '_').replace('-', '_') + implementation += f''' + parser.add_argument( + "--{param_name.replace('_', '-')}", + help="{inp}" + )''' + + implementation += f''' + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = {class_name}() + + # Execute skill + result = skill.execute(''' + + if skill_desc.get("inputs"): + for inp in skill_desc["inputs"]: + # Sanitize parameter names - remove special characters + param_name = ''.join(c if c.isalnum() or c in ' -_' else '' for c in inp.lower()) + param_name = param_name.replace(' ', '_').replace('-', '_') + implementation += f''' + {param_name}=args.{param_name},''' + + implementation += ''' + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() +''' + + return implementation + + def generate_tests(self, skill_desc: Dict[str, Any]) -> str: + """ + Generate test template + + Args: + skill_desc: Parsed skill description + + Returns: + Python test code as string + """ + skill_name = skill_desc["name"] + module_name = skill_name.replace('.', '_') + class_name = ''.join(word.capitalize() for word in skill_name.split('.')) + + tests = f'''#!/usr/bin/env python3 +""" +Tests for {skill_name} + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.{skill_name.replace('.', '_')} import {module_name} + + +class Test{class_name}: + """Tests for {class_name}""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = {module_name}.{class_name}() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["{module_name}.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + {module_name}.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "{skill_desc['purpose'][:50]}" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) +''' + + return tests + + def generate_skill_md(self, skill_desc: Dict[str, Any]) -> str: + """ + Generate SKILL.md + + Args: + skill_desc: Parsed skill description + + Returns: + Markdown content as string + """ + skill_name = skill_desc["name"] + + readme = f'''# {skill_name} + +{skill_desc["purpose"]} + +## Overview + +**Purpose:** {skill_desc["purpose"]} + +**Command:** `/{skill_name.replace('.', '/')}` + +## Usage + +### Basic Usage + +```bash +python3 skills/{skill_name.replace('.', '/')}/{skill_name.replace('.', '_')}.py +``` + +### With Arguments + +```bash +python3 skills/{skill_name.replace('.', '/')}/{skill_name.replace('.', '_')}.py \\ +''' + + if skill_desc.get("inputs"): + for inp in skill_desc["inputs"]: + param_name = inp.lower().replace(' ', '_').replace('-', '-') + readme += f' --{param_name} "value" \\\n' + + readme += ' --output-format json\n```\n\n' + + if skill_desc.get("inputs"): + readme += "## Inputs\n\n" + for inp in skill_desc["inputs"]: + readme += f"- **{inp}**\n" + readme += "\n" + + if skill_desc.get("outputs"): + readme += "## Outputs\n\n" + for out in skill_desc["outputs"]: + readme += f"- **{out}**\n" + readme += "\n" + + if skill_desc.get("artifact_consumes") or skill_desc.get("artifact_produces"): + readme += "## Artifact Metadata\n\n" + + if skill_desc.get("artifact_consumes"): + readme += "### Consumes\n\n" + for art in skill_desc["artifact_consumes"]: + readme += f"- `{art}`\n" + readme += "\n" + + if skill_desc.get("artifact_produces"): + readme += "### Produces\n\n" + for art in skill_desc["artifact_produces"]: + readme += f"- `{art}`\n" + readme += "\n" + + if skill_desc.get("examples"): + readme += "## Examples\n\n" + for example in skill_desc["examples"]: + readme += f"- {example}\n" + readme += "\n" + + if skill_desc.get("permissions"): + readme += "## Permissions\n\n" + for perm in skill_desc["permissions"]: + readme += f"- `{perm}`\n" + readme += "\n" + + if skill_desc.get("implementation_notes"): + readme += "## Implementation Notes\n\n" + readme += f"{skill_desc['implementation_notes']}\n\n" + + readme += f'''## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - {skill_name} +``` + +## Testing + +Run tests with: + +```bash +pytest skills/{skill_name.replace('.', '/')}/test_{skill_name.replace('.', '_')}.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* +''' + + return readme + + def validate_artifacts(self, skill_desc: Dict[str, Any]) -> List[str]: + """ + Validate that artifact types exist in the known registry. + + Args: + skill_desc: Parsed skill description + + Returns: + List of warning messages + """ + warnings = [] + + if not ARTIFACT_VALIDATION_AVAILABLE: + warnings.append( + "Artifact validation skipped: artifact.define skill not available" + ) + return warnings + + # Validate produced artifacts + for artifact_type in skill_desc.get("artifact_produces", []): + is_valid, warning = validate_artifact_type(artifact_type) + if not is_valid and warning: + warnings.append(f"Produces: {warning}") + + # Validate consumed artifacts + for artifact_type in skill_desc.get("artifact_consumes", []): + is_valid, warning = validate_artifact_type(artifact_type) + if not is_valid and warning: + warnings.append(f"Consumes: {warning}") + + return warnings + + def create_skill( + self, + description_path: str, + output_dir: Optional[str] = None, + requirement: Optional[RequirementInfo] = None + ) -> Dict[str, Any]: + """ + Create a complete skill from description + + Args: + description_path: Path to skill description file + output_dir: Output directory (default: skills/{name}/) + requirement: Optional requirement information for traceability + + Returns: + Summary of created files + """ + # Parse description + skill_desc = self.parse_description(description_path) + skill_name = skill_desc["name"] + + if not skill_name: + raise ValueError("Skill name is required") + + # Validate name format (domain.action) + if not re.match(r'^[a-z0-9]+\.[a-z0-9]+$', skill_name): + raise ValueError( + f"Skill name must be in domain.action format: {skill_name}" + ) + + # Validate artifact types + artifact_warnings = self.validate_artifacts(skill_desc) + if artifact_warnings: + print("\n⚠️ Artifact Validation Warnings:") + for warning in artifact_warnings: + print(f" {warning}") + print() + + # Determine output directory + if not output_dir: + output_dir = f"skills/{skill_name}" + + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + result = { + "skill_name": skill_name, + "created_files": [], + "errors": [], + "artifact_warnings": artifact_warnings + } + + # Generate and save skill.yaml + skill_yaml_content = self.generate_skill_yaml(skill_desc) + skill_yaml_path = output_path / "skill.yaml" + with open(skill_yaml_path, 'w') as f: + f.write(skill_yaml_content) + result["created_files"].append(str(skill_yaml_path)) + + # Generate and save implementation + impl_content = self.generate_implementation(skill_desc) + impl_path = output_path / f"{skill_name.replace('.', '_')}.py" + with open(impl_path, 'w') as f: + f.write(impl_content) + os.chmod(impl_path, 0o755) # Make executable + result["created_files"].append(str(impl_path)) + + # Generate and save tests + tests_content = self.generate_tests(skill_desc) + tests_path = output_path / f"test_{skill_name.replace('.', '_')}.py" + with open(tests_path, 'w') as f: + f.write(tests_content) + result["created_files"].append(str(tests_path)) + + # Generate and save SKILL.md + skill_md_content = self.generate_skill_md(skill_desc) + skill_md_path = output_path / "SKILL.md" + with open(skill_md_path, 'w') as f: + f.write(skill_md_content) + result["created_files"].append(str(skill_md_path)) + + # Log traceability if requirement provided + trace_id = None + if requirement: + try: + tracer = get_tracer() + trace_id = tracer.log_creation( + component_id=skill_name, + component_name=skill_name.replace(".", " ").title(), + component_type="skill", + component_version="0.1.0", + component_file_path=str(skill_yaml_path), + input_source_path=description_path, + created_by_tool="meta.skill", + created_by_version="0.1.0", + requirement=requirement, + tags=["skill", "auto-generated"], + project="Betty Framework" + ) + + # Log validation check + validation_details = { + "checks_performed": [ + {"name": "skill_structure", "status": "passed"}, + {"name": "artifact_metadata", "status": "passed"} + ] + } + + # Check for artifact metadata + if skill_desc.get("artifact_produces") or skill_desc.get("artifact_consumes"): + validation_details["checks_performed"].append({ + "name": "artifact_metadata_completeness", + "status": "passed", + "message": f"Produces: {len(skill_desc.get('artifact_produces', []))}, Consumes: {len(skill_desc.get('artifact_consumes', []))}" + }) + + tracer.log_verification( + component_id=skill_name, + check_type="validation", + tool="meta.skill", + result="passed", + details=validation_details + ) + + result["trace_id"] = trace_id + + except Exception as e: + print(f"⚠️ Warning: Could not log traceability: {e}") + + return result + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="meta.skill - Create skills from descriptions" + ) + parser.add_argument( + "description", + help="Path to skill description file (.md or .json)" + ) + parser.add_argument( + "-o", "--output", + help="Output directory (default: skills/{name}/)" + ) + + # Traceability arguments + parser.add_argument( + "--requirement-id", + help="Requirement identifier (e.g., REQ-2025-001)" + ) + parser.add_argument( + "--requirement-description", + help="What this skill accomplishes" + ) + parser.add_argument( + "--requirement-source", + help="Source document" + ) + parser.add_argument( + "--issue-id", + help="Issue tracking ID (e.g., JIRA-123)" + ) + parser.add_argument( + "--requested-by", + help="Who requested this" + ) + parser.add_argument( + "--rationale", + help="Why this is needed" + ) + + args = parser.parse_args() + + # Create requirement info if provided + requirement = None + if args.requirement_id and args.requirement_description: + requirement = RequirementInfo( + id=args.requirement_id, + description=args.requirement_description, + source=args.requirement_source, + issue_id=args.issue_id, + requested_by=args.requested_by, + rationale=args.rationale + ) + + creator = SkillCreator() + + print(f"🛠️ meta.skill - Creating skill from {args.description}") + + try: + result = creator.create_skill( + args.description, + output_dir=args.output, + requirement=requirement + ) + + print(f"\n✨ Skill '{result['skill_name']}' created successfully!\n") + + if result["created_files"]: + print("📄 Created files:") + for file in result["created_files"]: + print(f" - {file}") + + if result["errors"]: + print("\n⚠️ Warnings:") + for error in result["errors"]: + print(f" - {error}") + + if result.get("trace_id"): + print(f"\n📝 Traceability: {result['trace_id']}") + print(f" View trace: python3 betty/trace_cli.py show {result['skill_name']}") + + print(f"\n✅ Skill '{result['skill_name']}' is ready to use") + print(" Add to agent skills_available to use it.") + + except Exception as e: + print(f"\n❌ Error creating skill: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/agents/meta.suggest/README.md b/agents/meta.suggest/README.md new file mode 100644 index 0000000..4ca8daa --- /dev/null +++ b/agents/meta.suggest/README.md @@ -0,0 +1,510 @@ +# meta.suggest - Context-Aware Next-Step Recommender + +Helps Claude decide what to do next after an agent completes by analyzing context and suggesting compatible next steps. + +## Overview + +**meta.suggest** provides intelligent "what's next" recommendations by analyzing what just happened, what artifacts were produced, and what agents are compatible. It works with meta.compatibility to enable smart multi-agent orchestration. + +**What it does:** +- Analyzes context (what agent ran, what artifacts produced) +- Uses meta.compatibility to find compatible next steps +- Provides ranked suggestions with clear rationale +- Considers project state and user goals +- Detects warnings (gaps, isolated agents) +- Suggests project-wide improvements + +## Quick Start + +### Suggest Next Steps + +```bash +python3 agents/meta.suggest/meta_suggest.py \ + --context meta.agent \ + --artifacts agents/api.architect/agent.yaml +``` + +Output: +``` +Context: meta.agent +Produced: agent-definition + +🌟 Primary Suggestion: + Process with meta.compatibility + Rationale: meta.agent produces 'agent-definition' which meta.compatibility consumes + Priority: high + +🔄 Alternatives: + 1. Test the created artifact + Verify the artifact works as expected + + 2. Analyze compatibility + Understand what agents can work with meta.agent's outputs +``` + +### Analyze Project + +```bash +python3 agents/meta.suggest/meta_suggest.py --analyze-project +``` + +Output: +``` +📊 Project Analysis: + Total Agents: 7 + Total Artifacts: 16 + Relationships: 3 + Gaps: 5 + +💡 Suggestions (6): + 1. Create agent/skill to produce 'agent-description' + Consumed by 1 agents but no producers + Priority: medium + ... +``` + +## Commands + +### Suggest After Agent Runs + +```bash +python3 agents/meta.suggest/meta_suggest.py \ + --context AGENT_NAME \ + [--artifacts FILE1 FILE2...] \ + [--goal "USER_GOAL"] \ + [--format json|text] +``` + +**Parameters:** +- `--context` - Agent that just ran +- `--artifacts` - Artifact files that were produced (optional) +- `--goal` - User's goal for better suggestions (optional) +- `--format` - Output format (text or json) + +**Examples:** +```bash +# After meta.agent creates agent +python3 agents/meta.suggest/meta_suggest.py \ + --context meta.agent \ + --artifacts agents/my-agent/agent.yaml + +# After meta.artifact creates artifact type +python3 agents/meta.suggest/meta_suggest.py \ + --context meta.artifact \ + --artifacts schemas/my-artifact.json + +# With user goal +python3 agents/meta.suggest/meta_suggest.py \ + --context meta.agent \ + --goal "Create and validate API design agent" +``` + +### Analyze Project + +```bash +python3 agents/meta.suggest/meta_suggest.py --analyze-project [--format json|text] +``` + +Analyzes the entire agent ecosystem and suggests improvements: +- Agents to create +- Gaps to fill +- Documentation needs +- Ecosystem health + +## How It Works + +### 1. Context Analysis + +Determines what just happened: +- Which agent ran +- What artifacts were produced +- What artifact types are involved + +### 2. Compatibility Check + +Uses meta.compatibility to find: +- Agents that can consume the produced artifacts +- Agents that are compatible downstream +- Potential pipeline steps + +### 3. Suggestion Generation + +Creates suggestions based on: +- Compatible agents (high priority) +- Validation/testing options (medium priority) +- Gap-filling needs (low priority if applicable) + +### 4. Ranking + +Ranks suggestions by: +- Priority level (high > medium > low) +- Automation (automated > manual) +- Relevance to user goal + +### 5. Warning Generation + +Detects potential issues: +- Gaps in required artifacts +- Isolated agents (no compatible partners) +- Failed validations + +## Suggestion Types + +### 1. Process with Compatible Agent + +``` +🌟 Primary Suggestion: + Process with api.validator + Rationale: api.architect produces 'openapi-spec' which api.validator consumes +``` + +Automatically suggests running compatible agents. + +### 2. Validate/Test Artifact + +``` + Test the created artifact + Rationale: Verify the artifact works as expected +``` + +Suggests testing when creation-type agents run. + +### 3. Analyze Compatibility + +``` + Analyze compatibility + Rationale: Understand what agents can work with meta.agent's outputs + Command: python3 agents/meta.compatibility/meta_compatibility.py analyze meta.agent +``` + +Suggests understanding the ecosystem. + +### 4. Fill Gaps + +``` + Create producer for 'agent-description' + Rationale: No agents produce 'agent-description' (required by meta.agent) +``` + +Suggests creating missing components. + +## Output Structure + +### Text Format + +``` +Context: AGENT_NAME +Produced: artifact-type-1, artifact-type-2 + +🌟 Primary Suggestion: + ACTION + Rationale: WHY + [Command: HOW (if automated)] + Priority: LEVEL + +🔄 Alternatives: + 1. ACTION + Rationale: WHY + +⚠️ Warnings: + • WARNING_MESSAGE +``` + +### JSON Format + +```json +{ + "context": { + "agent": "meta.agent", + "artifacts_produced": ["agents/my-agent/agent.yaml"], + "artifact_types": ["agent-definition"], + "timestamp": "2025-10-24T..." + }, + "suggestions": [ + { + "action": "Process with meta.compatibility", + "agent": "meta.compatibility", + "rationale": "...", + "priority": "high", + "command": "..." + } + ], + "primary_suggestion": {...}, + "alternatives": [...], + "warnings": [...] +} +``` + +## Integration + +### With meta.compatibility + +meta.suggest uses meta.compatibility for discovery: + +```python +# Internal call +compatibility = meta.compatibility.find_compatible(agent_name) + +# Use compatible agents for suggestions +for compatible in compatibility.get("can_feed_to", []): + suggest(f"Process with {compatible['agent']}") +``` + +### With Claude + +Claude can call meta.suggest after any agent: + +``` +User: Create an API design agent +Claude: *runs meta.agent* +Claude: *calls meta.suggest --context meta.agent* +Claude: I've created the agent. Would you like me to: + 1. Analyze its compatibility + 2. Test it + 3. Add documentation +``` + +### In Workflows + +Use in shell scripts: + +```bash +#!/bin/bash +# Create and analyze agent + +# Step 1: Create agent +python3 agents/meta.agent/meta_agent.py description.md + +# Step 2: Get suggestions +SUGGESTIONS=$(python3 agents/meta.suggest/meta_suggest.py \ + --context meta.agent \ + --format json) + +# Step 3: Extract primary suggestion +PRIMARY=$(echo "$SUGGESTIONS" | jq -r '.primary_suggestion.command') + +# Step 4: Run it +eval "$PRIMARY" +``` + +## Common Workflows + +### Workflow 1: Agent Creation Pipeline + +```bash +# Create agent +python3 agents/meta.agent/meta_agent.py my_agent.md + +# Get suggestions +python3 agents/meta.suggest/meta_suggest.py \ + --context meta.agent \ + --artifacts agents/my-agent/agent.yaml + +# Follow primary suggestion +python3 agents/meta.compatibility/meta_compatibility.py analyze my-agent +``` + +### Workflow 2: Continuous Improvement + +```bash +# Analyze project +python3 agents/meta.suggest/meta_suggest.py --analyze-project > improvements.txt + +# Review suggestions +cat improvements.txt + +# Implement top suggestions +# (create missing agents, fill gaps, etc.) +``` + +### Workflow 3: Goal-Oriented Orchestration + +```bash +# Define goal +GOAL="Design, validate, and implement an API" + +# Get suggestions for goal +python3 agents/meta.suggest/meta_suggest.py \ + --goal "$GOAL" \ + --format json > pipeline.json + +# Execute suggested pipeline +# (extract steps from pipeline.json and run) +``` + +## Artifact Types + +### Consumes + +- **compatibility-graph** - Agent compatibility information + - From: meta.compatibility + +- **agent-definition** - Agent that just ran + - Pattern: `agents/*/agent.yaml` + +### Produces + +- **suggestion-report** - Next-step recommendations + - Pattern: `*.suggestions.json` + - Schema: `schemas/suggestion-report.json` + +## Understanding Suggestions + +### Priority Levels + +**High** - Should probably do this +- Compatible agent waiting +- Validation needed +- Next logical step + +**Medium** - Good to do +- Analyze compatibility +- Understand ecosystem +- Non-critical validation + +**Low** - Nice to have +- Fill gaps +- Documentation +- Future improvements + +### Automated vs Manual + +**Automated** - Has command to run +``` +Command: python3 agents/meta.compatibility/... +``` + +**Manual** - Requires user action +``` +(No command - manual action required) +``` + +### Rationale + +Always includes "why" for each suggestion: +``` +Rationale: meta.agent produces 'agent-definition' which meta.compatibility consumes +``` + +Helps Claude and users understand the reasoning. + +## Tips & Best Practices + +### Providing Context + +More context = better suggestions: + +✅ **Good:** +```bash +--context meta.agent \ +--artifacts agents/my-agent/agent.yaml \ +--goal "Create and validate agent" +``` + +❌ **Minimal:** +```bash +--context meta.agent +``` + +### Interpreting Warnings + +**Gaps warning:** +``` +⚠️ meta.agent requires artifacts that aren't produced by any agent +``` +This is often expected for user inputs. Not always a problem. + +**Isolated warning:** +``` +⚠️ my-agent has no compatible agents +``` +This suggests the agent uses non-standard artifact types or no other agents exist yet. + +### Using Suggestions + +1. **Review primary suggestion first** - Usually the best option +2. **Consider alternatives** - May be better for your specific case +3. **Check warnings** - Understand potential issues +4. **Verify commands** - Review before running automated suggestions + +## Troubleshooting + +### No suggestions returned + +``` +Error: Could not determine relevant agents for goal +``` + +**Causes:** +- Agent has no compatible downstream agents +- Artifact types are all user-provided inputs +- No other agents in ecosystem + +**Solutions:** +- Create more agents +- Use standard artifact types +- Check agent artifact_metadata + +### Incorrect suggestions + +If suggestions don't make sense: +- Verify agent artifact_metadata is correct +- Check meta.compatibility output directly +- Ensure artifact types are registered + +### Empty project analysis + +``` +Total Agents: 0 +``` + +**Cause:** No agents found in `agents/` directory + +**Solution:** Create agents using meta.agent or manually + +## Architecture + +``` +meta.suggest + ├─ Uses: meta.compatibility (discovery) + ├─ Analyzes: context and artifacts + ├─ Produces: ranked suggestions + └─ Helps: Claude make decisions +``` + +## Examples + +```bash +# Example 1: After creating agent +python3 agents/meta.agent/meta_agent.py examples/api_architect_description.md +python3 agents/meta.suggest/meta_suggest.py --context meta.agent + +# Example 2: After creating artifact type +python3 agents/meta.artifact/meta_artifact.py create artifact.md +python3 agents/meta.suggest/meta_suggest.py --context meta.artifact + +# Example 3: Project health check +python3 agents/meta.suggest/meta_suggest.py --analyze-project + +# Example 4: Export to JSON +python3 agents/meta.suggest/meta_suggest.py \ + --context meta.agent \ + --format json > suggestions.json +``` + +## Related Documentation + +- [META_AGENTS.md](../../docs/META_AGENTS.md) - Meta-agent ecosystem +- [meta.compatibility README](../meta.compatibility/README.md) - Compatibility analyzer +- [ARTIFACT_STANDARDS.md](../../docs/ARTIFACT_STANDARDS.md) - Artifact system +- [suggestion-report schema](../../schemas/suggestion-report.json) + +## How Claude Uses This + +After any agent completes: +1. Claude calls meta.suggest with context +2. Reviews suggestions and rationale +3. Presents options to user or auto-executes +4. Makes intelligent orchestration decisions + +meta.suggest is Claude's assistant for "what's next" decisions! diff --git a/agents/meta.suggest/agent.yaml b/agents/meta.suggest/agent.yaml new file mode 100644 index 0000000..049ccda --- /dev/null +++ b/agents/meta.suggest/agent.yaml @@ -0,0 +1,127 @@ +name: meta.suggest +version: 0.1.0 +description: | + Context-aware next-step recommender that helps Claude decide what to do next + after an agent completes. + + Analyzes current context, produced artifacts, and project state to suggest + compatible agents and workflows. Works with meta.compatibility to provide + intelligent orchestration recommendations. + +artifact_metadata: + consumes: + - type: compatibility-graph + description: "Agent compatibility information from meta.compatibility" + + - type: agent-definition + description: "Agent that just ran" + + produces: + - type: suggestion-report + file_pattern: "*.suggestions.json" + content_type: "application/json" + schema: "schemas/suggestion-report.json" + description: "Context-aware recommendations for next steps" + +status: draft +reasoning_mode: iterative +capabilities: + - Analyze produced artifacts to understand project context + - Recommend next agents or workflows with supporting rationale + - Highlight gaps and dependencies to maintain delivery momentum +skills_available: + - meta.compatibility # Analyze compatibility + - artifact.define # Understand artifacts + +permissions: + - filesystem:read + +system_prompt: | + You are meta.suggest, the context-aware next-step recommender. + + After an agent completes its work, you help Claude decide what to do next by + analyzing what artifacts were produced and suggesting compatible next steps. + + ## Your Responsibilities + + 1. **Analyze Context** + - What agent just ran? + - What artifacts were produced? + - What's the current project state? + - What might the user want to do next? + + 2. **Suggest Next Steps** + - Use meta.compatibility to find compatible agents + - Rank suggestions by relevance and usefulness + - Provide clear rationale for each suggestion + - Consider common workflows + + 3. **Be Smart About Context** + - If validation failed, don't suggest proceeding + - If artifacts were created, suggest consumers + - If gaps were detected, suggest filling them + - Consider the user's likely goals + + ## Commands You Support + + **Suggest next steps after agent ran:** + ```bash + /meta/suggest --context meta.agent --artifacts agents/my-agent/agent.yaml + ``` + + **Analyze project and suggest:** + ```bash + /meta/suggest --analyze-project + ``` + + **Suggest for specific goal:** + ```bash + /meta/suggest --goal "Design and implement an API" + ``` + + ## Suggestion Criteria + + Good suggestions: + - Are relevant to what just happened + - Use artifacts that were produced + - Follow logical workflow order + - Provide clear value to the user + - Include validation/quality checks when appropriate + + Bad suggestions: + - Suggest proceeding after failures + - Ignore produced artifacts + - Suggest irrelevant agents + - Don't explain why + + ## Output Format + + Always provide: + - **Primary suggestion**: Best next step with strong rationale + - **Alternative suggestions**: 2-3 other options + - **Rationale**: Why each suggestion makes sense + - **Artifacts needed**: What inputs each option requires + - **Expected outcome**: What each option will produce + + ## Example Interactions + + **Context:** meta.agent just created agent.yaml for new agent + + **Suggestions:** + 1. Validate the agent (meta.compatibility analyze) + - Rationale: Ensure agent has proper artifact compatibility + - Needs: agent.yaml (already produced) + - Produces: compatibility analysis + + 2. Test the agent (agent.run) + - Rationale: See if agent works as expected + - Needs: agent.yaml + test inputs + - Produces: execution results + + 3. Document the agent (manual) + - Rationale: Add examples and usage guide + - Needs: Understanding of agent purpose + - Produces: Enhanced README.md + + Remember: You're Claude's assistant for orchestration. Help Claude make + smart decisions about what to do next based on context and compatibility. diff --git a/agents/meta.suggest/meta_suggest.py b/agents/meta.suggest/meta_suggest.py new file mode 100755 index 0000000..022d4b7 --- /dev/null +++ b/agents/meta.suggest/meta_suggest.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +""" +meta.suggest - Context-Aware Next-Step Recommender + +Helps Claude decide what to do next after an agent completes by analyzing +context and suggesting compatible next steps. +""" + +import json +import yaml +import sys +import os +from pathlib import Path +from typing import Dict, List, Any, Optional, Set +from datetime import datetime + +# Add parent directory to path for imports +parent_dir = str(Path(__file__).parent.parent.parent) +sys.path.insert(0, parent_dir) + +# Import meta.compatibility +meta_comp_path = parent_dir + "/agents/meta.compatibility" +sys.path.insert(0, meta_comp_path) +import meta_compatibility + + +class SuggestionEngine: + """Context-aware suggestion engine""" + + def __init__(self, base_dir: str = "."): + """Initialize with base directory""" + self.base_dir = Path(base_dir) + self.compatibility_analyzer = meta_compatibility.CompatibilityAnalyzer(base_dir) + self.compatibility_analyzer.scan_agents() + self.compatibility_analyzer.build_compatibility_map() + + def suggest_next_steps( + self, + context_agent: str, + artifacts_produced: Optional[List[str]] = None, + goal: Optional[str] = None + ) -> Dict[str, Any]: + """ + Suggest next steps based on context + + Args: + context_agent: Agent that just ran + artifacts_produced: List of artifact file paths produced + goal: Optional user goal + + Returns: + Suggestion report with recommendations + """ + # Get compatibility info for the agent + compatibility = self.compatibility_analyzer.find_compatible(context_agent) + + if "error" in compatibility: + return { + "error": compatibility["error"], + "context": { + "agent": context_agent, + "artifacts": artifacts_produced or [] + } + } + + # Determine artifact types produced + artifact_types = set() + if artifacts_produced: + artifact_types = self._infer_artifact_types(artifacts_produced) + else: + artifact_types = set(compatibility.get("produces", [])) + + suggestions = [] + + # Suggestion 1: Validate/analyze what was created + if context_agent not in ["meta.compatibility", "meta.suggest"]: + suggestions.append({ + "action": "Analyze compatibility", + "agent": "meta.compatibility", + "command": f"python3 agents/meta.compatibility/meta_compatibility.py analyze {context_agent}", + "rationale": f"Understand what agents can work with {context_agent}'s outputs", + "artifacts_needed": [], + "produces": ["compatibility-graph"], + "priority": "medium", + "estimated_duration": "< 1 minute" + }) + + # Suggestion 2: Use compatible agents + can_feed_to = compatibility.get("can_feed_to", []) + for compatible in can_feed_to[:3]: # Top 3 + next_agent = compatible["agent"] + artifact = compatible["artifact"] + + suggestions.append({ + "action": f"Process with {next_agent}", + "agent": next_agent, + "rationale": compatible["rationale"], + "artifacts_needed": [artifact], + "produces": self._get_agent_produces(next_agent), + "priority": "high", + "estimated_duration": "varies" + }) + + # Suggestion 3: If agent created something, suggest testing/validation + if artifact_types and context_agent in ["meta.agent", "meta.artifact"]: + suggestions.append({ + "action": "Test the created artifact", + "rationale": "Verify the artifact works as expected", + "artifacts_needed": list(artifact_types), + "priority": "high", + "manual": True + }) + + # Suggestion 4: If gaps exist, suggest filling them + gaps = compatibility.get("gaps", []) + if gaps: + for gap in gaps[:2]: # Top 2 gaps + suggestions.append({ + "action": f"Create producer for '{gap['artifact']}'", + "rationale": gap["issue"], + "severity": gap.get("severity", "medium"), + "priority": "low", + "manual": True + }) + + # Rank suggestions + suggestions = self._rank_suggestions(suggestions, goal) + + # Build report + report = { + "context": { + "agent": context_agent, + "artifacts_produced": artifacts_produced or [], + "artifact_types": list(artifact_types), + "timestamp": datetime.now().isoformat() + }, + "suggestions": suggestions, + "primary_suggestion": suggestions[0] if suggestions else None, + "alternatives": suggestions[1:4] if len(suggestions) > 1 else [], + "warnings": self._generate_warnings(context_agent, compatibility, gaps) + } + + return report + + def _infer_artifact_types(self, artifact_paths: List[str]) -> Set[str]: + """Infer artifact types from file paths""" + types = set() + + for path in artifact_paths: + path_lower = path.lower() + + # Pattern matching + if ".openapi." in path_lower: + types.add("openapi-spec") + elif "agent.yaml" in path_lower: + types.add("agent-definition") + elif "readme.md" in path_lower: + if "agent" in path_lower: + types.add("agent-documentation") + elif ".validation." in path_lower: + types.add("validation-report") + elif ".optimization." in path_lower: + types.add("optimization-report") + elif ".compatibility." in path_lower: + types.add("compatibility-graph") + elif ".pipeline." in path_lower: + types.add("pipeline-suggestion") + elif ".workflow." in path_lower: + types.add("workflow-definition") + + return types + + def _get_agent_produces(self, agent_name: str) -> List[str]: + """Get what an agent produces""" + if agent_name in self.compatibility_analyzer.agents: + agent_def = self.compatibility_analyzer.agents[agent_name] + produces, _ = self.compatibility_analyzer.extract_artifacts(agent_def) + return list(produces) + return [] + + def _rank_suggestions(self, suggestions: List[Dict], goal: Optional[str] = None) -> List[Dict]: + """Rank suggestions by relevance""" + priority_order = {"high": 3, "medium": 2, "low": 1} + + # Sort by priority, then by manual (auto first) + return sorted( + suggestions, + key=lambda s: ( + -priority_order.get(s.get("priority", "medium"), 2), + s.get("manual", False) # Auto suggestions first + ) + ) + + def _generate_warnings( + self, + agent: str, + compatibility: Dict, + gaps: List[Dict] + ) -> List[Dict]: + """Generate warnings based on context""" + warnings = [] + + # Warn about gaps + if gaps: + warnings.append({ + "type": "gaps", + "message": f"{agent} requires artifacts that aren't produced by any agent", + "details": [g["artifact"] for g in gaps], + "severity": "medium" + }) + + # Warn if no compatible agents + if not compatibility.get("can_feed_to") and not compatibility.get("can_receive_from"): + warnings.append({ + "type": "isolated", + "message": f"{agent} has no compatible agents", + "details": "This agent can't be used in multi-agent pipelines", + "severity": "low" + }) + + return warnings + + def analyze_project(self) -> Dict[str, Any]: + """Analyze entire project and suggest improvements""" + # Generate compatibility graph + graph = self.compatibility_analyzer.generate_compatibility_graph() + + suggestions = [] + + # Suggest filling gaps + for gap in graph.get("gaps", []): + suggestions.append({ + "action": f"Create agent/skill to produce '{gap['artifact']}'", + "rationale": gap["issue"], + "priority": "medium", + "impact": f"Enables {len(gap.get('consumers', []))} agents" + }) + + # Suggest creating more agents if few exist + if graph["metadata"]["total_agents"] < 5: + suggestions.append({ + "action": "Create more agents using meta.agent", + "rationale": "Expand agent ecosystem for more capabilities", + "priority": "low" + }) + + # Suggest documentation if gaps exist + if graph.get("gaps"): + suggestions.append({ + "action": "Document artifact standards for gaps", + "rationale": "Clarify requirements for missing artifacts", + "priority": "low" + }) + + return { + "project_analysis": { + "total_agents": graph["metadata"]["total_agents"], + "total_artifacts": graph["metadata"]["total_artifact_types"], + "total_relationships": len(graph["relationships"]), + "total_gaps": len(graph["gaps"]) + }, + "suggestions": suggestions, + "gaps": graph["gaps"], + "timestamp": datetime.now().isoformat() + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="meta.suggest - Context-Aware Next-Step Recommender" + ) + + parser.add_argument( + "--context", + help="Agent that just ran" + ) + parser.add_argument( + "--artifacts", + nargs="+", + help="Artifacts that were produced" + ) + parser.add_argument( + "--goal", + help="User's goal (for better suggestions)" + ) + parser.add_argument( + "--analyze-project", + action="store_true", + help="Analyze entire project and suggest improvements" + ) + parser.add_argument( + "--format", + choices=["json", "text"], + default="text", + help="Output format" + ) + + args = parser.parse_args() + + engine = SuggestionEngine() + + if args.analyze_project: + print("🔍 Analyzing project...\n") + result = engine.analyze_project() + + if args.format == "text": + print(f"📊 Project Analysis:") + print(f" Total Agents: {result['project_analysis']['total_agents']}") + print(f" Total Artifacts: {result['project_analysis']['total_artifacts']}") + print(f" Relationships: {result['project_analysis']['total_relationships']}") + print(f" Gaps: {result['project_analysis']['total_gaps']}") + + if result.get("suggestions"): + print(f"\n💡 Suggestions ({len(result['suggestions'])}):") + for i, suggestion in enumerate(result["suggestions"], 1): + print(f"\n {i}. {suggestion['action']}") + print(f" {suggestion['rationale']}") + print(f" Priority: {suggestion['priority']}") + + else: + print(json.dumps(result, indent=2)) + + elif args.context: + print(f"💡 Suggesting next steps after '{args.context}'...\n") + result = engine.suggest_next_steps( + args.context, + args.artifacts, + args.goal + ) + + if args.format == "text": + if "error" in result: + print(f"❌ Error: {result['error']}") + return + + print(f"Context: {result['context']['agent']}") + if result['context']['artifact_types']: + print(f"Produced: {', '.join(result['context']['artifact_types'])}") + + if result.get("primary_suggestion"): + print(f"\n🌟 Primary Suggestion:") + ps = result["primary_suggestion"] + print(f" {ps['action']}") + print(f" Rationale: {ps['rationale']}") + if not ps.get("manual"): + print(f" Command: {ps.get('command', 'N/A')}") + print(f" Priority: {ps['priority']}") + + if result.get("alternatives"): + print(f"\n🔄 Alternatives:") + for i, alt in enumerate(result["alternatives"], 1): + print(f"\n {i}. {alt['action']}") + print(f" {alt['rationale']}") + + if result.get("warnings"): + print(f"\n⚠️ Warnings:") + for warning in result["warnings"]: + print(f" • {warning['message']}") + + else: + print(json.dumps(result, indent=2)) + + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/agents/operations.orchestrator/README.md b/agents/operations.orchestrator/README.md new file mode 100644 index 0000000..14a9d49 --- /dev/null +++ b/agents/operations.orchestrator/README.md @@ -0,0 +1,46 @@ +# Operations.Orchestrator Agent + +Orchestrates operational workflows including builds, deployments, and monitoring + +## Purpose + +This orchestrator agent coordinates complex operations workflows by composing and sequencing multiple skills. It handles the complete lifecycle from planning through execution and validation. + +## Capabilities + +- Coordinate build and deployment pipelines +- Manage infrastructure as code workflows +- Orchestrate monitoring and alerting +- Handle incident response and remediation +- Coordinate release management + +## Available Skills + +- `build.optimize` +- `workflow.orchestrate` +- `workflow.compose` +- `workflow.validate` +- `git.createpr` +- `git.cleanupbranches` +- `telemetry.capture` + +## Usage + +This agent uses iterative reasoning to: +1. Analyze requirements +2. Plan execution steps +3. Coordinate skill execution +4. Validate results +5. Handle errors and retries + +## Status + +**Generated**: Auto-generated from taxonomy gap analysis + +## Next Steps + +- [ ] Review and refine capabilities +- [ ] Test with real workflows +- [ ] Add domain-specific examples +- [ ] Integrate with existing agents +- [ ] Document best practices diff --git a/agents/operations.orchestrator/agent.yaml b/agents/operations.orchestrator/agent.yaml new file mode 100644 index 0000000..ef7c821 --- /dev/null +++ b/agents/operations.orchestrator/agent.yaml @@ -0,0 +1,56 @@ +name: operations.orchestrator +version: 0.1.0 +description: Orchestrates operational workflows including builds, deployments, and + monitoring +capabilities: +- Coordinate build and deployment pipelines +- Manage infrastructure as code workflows +- Orchestrate monitoring and alerting +- Handle incident response and remediation +- Coordinate release management +skills_available: +- build.optimize +- workflow.orchestrate +- workflow.compose +- workflow.validate +- git.createpr +- git.cleanupbranches +- telemetry.capture +reasoning_mode: iterative +tags: +- operations +- orchestration +- devops +- deployment +workflow_pattern: '1. Analyze incoming request and requirements + + 2. Identify relevant operations skills and workflows + + 3. Compose multi-step execution plan + + 4. Execute skills in coordinated sequence + + 5. Validate intermediate results + + 6. Handle errors and retry as needed + + 7. Return comprehensive results' +example_task: "Input: \"Complete operations workflow from start to finish\"\n\nAgent\ + \ will:\n1. Break down the task into stages\n2. Select appropriate skills for each\ + \ stage\n3. Execute create \u2192 validate \u2192 review \u2192 publish lifecycle\n\ + 4. Monitor progress and handle failures\n5. Generate comprehensive reports" +error_handling: + timeout_seconds: 300 + retry_strategy: exponential_backoff + max_retries: 3 +output: + success: + - Operations workflow results + - Execution logs and metrics + - Validation reports + - Generated artifacts + failure: + - Error details and stack traces + - Partial results (if available) + - Remediation suggestions +status: generated diff --git a/agents/security.architect/README.md b/agents/security.architect/README.md new file mode 100644 index 0000000..897b378 --- /dev/null +++ b/agents/security.architect/README.md @@ -0,0 +1,72 @@ +# Security.Architect Agent + +## Purpose + +Create comprehensive security architecture and assessment artifacts including threat models, security architecture diagrams, penetration testing reports, vulnerability management plans, and incident response plans. Applies security frameworks (STRIDE, NIST, ISO 27001, OWASP) and creates artifacts ready for security review and compliance audit. + +## Skills + +This agent uses the following skills: + + +## Artifact Flow + +### Consumes + +- `System or application description` +- `Architecture components and data flows` +- `Security requirements or compliance needs` +- `Assets and data classification` +- `Existing security controls` +- `Threat intelligence or vulnerability data` + +### Produces + +- `threat-model: STRIDE-based threat model with attack vectors, risk scoring, and security controls` +- `security-architecture-diagram: Security architecture with trust boundaries, security zones, and control points` +- `penetration-testing-report: Penetration test findings with CVSS scores and remediation recommendations` +- `vulnerability-management-plan: Vulnerability management program with policies and procedures` +- `incident-response-plan: Incident response playbook with roles, procedures, and escalation` +- `security-assessment: Security posture assessment against frameworks` +- `zero-trust-design: Zero trust architecture design with identity, device, and data controls` +- `compliance-matrix: Compliance mapping to regulatory requirements` + +## Example Use Cases + +- System description with components (API gateway, tokenization service, payment processor) +- Trust boundaries (external, DMZ, internal) +- Asset inventory (credit card data, transaction records) +- STRIDE threat catalog with 15-20 threats +- Security controls mapped to each threat +- Residual risk assessment +- PCI-DSS compliance mapping +- Network segmentation and security zones +- Identity and access management (IAM) controls +- Data encryption (at rest and in transit) +- Tenant isolation mechanisms +- Logging and monitoring infrastructure +- Compliance controls for SOC 2 +- Incident classification and severity levels +- Response team roles and responsibilities +- Incident response procedures by type +- Communication and escalation protocols +- Forensics and evidence collection +- Post-incident review process + +## Usage + +```bash +# Activate the agent +/agent security.architect + +# Or invoke directly +betty agent run security.architect --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/security.architect/agent.yaml b/agents/security.architect/agent.yaml new file mode 100644 index 0000000..db8d04c --- /dev/null +++ b/agents/security.architect/agent.yaml @@ -0,0 +1,65 @@ +name: security.architect +version: 0.1.0 +description: Create comprehensive security architecture and assessment artifacts including + threat models, security architecture diagrams, penetration testing reports, vulnerability + management plans, and incident response plans. Applies security frameworks (STRIDE, + NIST, ISO 27001, OWASP) and creates artifacts ready for security review and compliance + audit. +status: draft +reasoning_mode: iterative +capabilities: + - Perform structured threat modeling and control gap assessments + - Produce security architecture and testing documentation for reviews + - Recommend remediation and governance improvements for security programs +skills_available: + - artifact.create + - artifact.validate + - artifact.review +permissions: + - filesystem:read + - filesystem:write +artifact_metadata: + consumes: + - type: System or application description + description: Input artifact of type System or application description + - type: Architecture components and data flows + description: Input artifact of type Architecture components and data flows + - type: Security requirements or compliance needs + description: Input artifact of type Security requirements or compliance needs + - type: Assets and data classification + description: Input artifact of type Assets and data classification + - type: Existing security controls + description: Input artifact of type Existing security controls + - type: Threat intelligence or vulnerability data + description: Input artifact of type Threat intelligence or vulnerability data + produces: + - type: 'threat-model: STRIDE-based threat model with attack vectors, risk scoring, + and security controls' + description: 'Output artifact of type threat-model: STRIDE-based threat model + with attack vectors, risk scoring, and security controls' + - type: 'security-architecture-diagram: Security architecture with trust boundaries, + security zones, and control points' + description: 'Output artifact of type security-architecture-diagram: Security + architecture with trust boundaries, security zones, and control points' + - type: 'penetration-testing-report: Penetration test findings with CVSS scores + and remediation recommendations' + description: 'Output artifact of type penetration-testing-report: Penetration + test findings with CVSS scores and remediation recommendations' + - type: 'vulnerability-management-plan: Vulnerability management program with policies + and procedures' + description: 'Output artifact of type vulnerability-management-plan: Vulnerability + management program with policies and procedures' + - type: 'incident-response-plan: Incident response playbook with roles, procedures, + and escalation' + description: 'Output artifact of type incident-response-plan: Incident response + playbook with roles, procedures, and escalation' + - type: 'security-assessment: Security posture assessment against frameworks' + description: 'Output artifact of type security-assessment: Security posture assessment + against frameworks' + - type: 'zero-trust-design: Zero trust architecture design with identity, device, + and data controls' + description: 'Output artifact of type zero-trust-design: Zero trust architecture + design with identity, device, and data controls' + - type: 'compliance-matrix: Compliance mapping to regulatory requirements' + description: 'Output artifact of type compliance-matrix: Compliance mapping to + regulatory requirements' diff --git a/agents/security.orchestrator/README.md b/agents/security.orchestrator/README.md new file mode 100644 index 0000000..c3e9891 --- /dev/null +++ b/agents/security.orchestrator/README.md @@ -0,0 +1,43 @@ +# Security.Orchestrator Agent + +Orchestrates security workflows including audits, compliance checks, and vulnerability management + +## Purpose + +This orchestrator agent coordinates complex security workflows by composing and sequencing multiple skills. It handles the complete lifecycle from planning through execution and validation. + +## Capabilities + +- Coordinate security audits and assessments +- Manage compliance validation workflows +- Orchestrate vulnerability scanning and remediation +- Handle security documentation generation +- Coordinate access control and policy enforcement + +## Available Skills + +- `policy.enforce` +- `artifact.validate` +- `artifact.review` +- `audit.log` + +## Usage + +This agent uses iterative reasoning to: +1. Analyze requirements +2. Plan execution steps +3. Coordinate skill execution +4. Validate results +5. Handle errors and retries + +## Status + +**Generated**: Auto-generated from taxonomy gap analysis + +## Next Steps + +- [ ] Review and refine capabilities +- [ ] Test with real workflows +- [ ] Add domain-specific examples +- [ ] Integrate with existing agents +- [ ] Document best practices diff --git a/agents/security.orchestrator/agent.yaml b/agents/security.orchestrator/agent.yaml new file mode 100644 index 0000000..1f9fb5e --- /dev/null +++ b/agents/security.orchestrator/agent.yaml @@ -0,0 +1,53 @@ +name: security.orchestrator +version: 0.1.0 +description: Orchestrates security workflows including audits, compliance checks, + and vulnerability management +capabilities: +- Coordinate security audits and assessments +- Manage compliance validation workflows +- Orchestrate vulnerability scanning and remediation +- Handle security documentation generation +- Coordinate access control and policy enforcement +skills_available: +- policy.enforce +- artifact.validate +- artifact.review +- audit.log +reasoning_mode: iterative +tags: +- security +- orchestration +- compliance +- audit +workflow_pattern: '1. Analyze incoming request and requirements + + 2. Identify relevant security skills and workflows + + 3. Compose multi-step execution plan + + 4. Execute skills in coordinated sequence + + 5. Validate intermediate results + + 6. Handle errors and retry as needed + + 7. Return comprehensive results' +example_task: "Input: \"Complete security workflow from start to finish\"\n\nAgent\ + \ will:\n1. Break down the task into stages\n2. Select appropriate skills for each\ + \ stage\n3. Execute create \u2192 validate \u2192 review \u2192 publish lifecycle\n\ + 4. Monitor progress and handle failures\n5. Generate comprehensive reports" +error_handling: + timeout_seconds: 300 + retry_strategy: exponential_backoff + max_retries: 3 +output: + success: + - Security workflow results + - Execution logs and metrics + - Validation reports + - Generated artifacts + failure: + - Error details and stack traces + - Partial results (if available) + - Remediation suggestions +status: generated diff --git a/agents/strategy.architect/README.md b/agents/strategy.architect/README.md new file mode 100644 index 0000000..3bbd23b --- /dev/null +++ b/agents/strategy.architect/README.md @@ -0,0 +1,71 @@ +# Strategy.Architect Agent + +## Purpose + +Create comprehensive business strategy and planning artifacts including business cases, portfolio roadmaps, market analyses, competitive assessments, and strategic planning documents. Leverages financial modeling (NPV, IRR, ROI) and industry frameworks (PMBOK, SAFe, BCG Matrix) to produce executive-ready strategic deliverables. + +## Skills + +This agent uses the following skills: + + +## Artifact Flow + +### Consumes + +- `Initiative or project description` +- `Problem statement or opportunity` +- `Target business outcomes` +- `Budget range or financial constraints` +- `Market research data` +- `Competitive intelligence` +- `Stakeholder requirements` + +### Produces + +- `business-case: Comprehensive business justification with financial analysis, ROI model, risk assessment, and recommendation` +- `portfolio-roadmap: Strategic multi-initiative roadmap with timeline, dependencies, and resource allocation` +- `market-analysis: Market opportunity assessment with sizing, trends, and target segments` +- `competitive-analysis: Competitive landscape analysis with positioning and differentiation` +- `feasibility-study: Technical and business feasibility assessment` +- `strategic-plan: Multi-year strategic planning document with objectives and key results` +- `value-proposition-canvas: Customer value proposition and fit analysis` +- `roi-model: Financial return on investment model with multi-year projections` + +## Example Use Cases + +- Executive summary for C-suite +- Problem statement with impact analysis +- Proposed solution with scope +- Financial analysis (costs $500K, benefits $300K annually, 18mo payback) +- Risk assessment with mitigation +- Implementation timeline +- Recommendation and next steps +- Strategic alignment to business objectives +- Three major initiatives with phases +- Timeline with milestones and dependencies +- Resource allocation across initiatives +- Budget distribution ($5M across 18 months) +- Risk and dependency management +- Success metrics and KPIs +- market-analysis.yaml with market sizing, growth trends, target segments +- competitive-analysis.yaml with competitor positioning, SWOT analysis +- value-proposition-canvas.yaml with customer jobs, pains, gains + +## Usage + +```bash +# Activate the agent +/agent strategy.architect + +# Or invoke directly +betty agent run strategy.architect --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/strategy.architect/agent.yaml b/agents/strategy.architect/agent.yaml new file mode 100644 index 0000000..c6cb6bc --- /dev/null +++ b/agents/strategy.architect/agent.yaml @@ -0,0 +1,65 @@ +name: strategy.architect +version: 0.1.0 +description: Create comprehensive business strategy and planning artifacts including + business cases, portfolio roadmaps, market analyses, competitive assessments, and + strategic planning documents. Leverages financial modeling (NPV, IRR, ROI) and industry + frameworks (PMBOK, SAFe, BCG Matrix) to produce executive-ready strategic deliverables. +status: draft +reasoning_mode: iterative +capabilities: + - Build financial models and strategic roadmaps aligned to business objectives + - Analyze market and competitive data to inform executive decisions + - Produce governance-ready artifacts with risks, dependencies, and recommendations +skills_available: + - artifact.create + - artifact.validate + - artifact.review +permissions: + - filesystem:read + - filesystem:write +artifact_metadata: + consumes: + - type: Initiative or project description + description: Input artifact of type Initiative or project description + - type: Problem statement or opportunity + description: Input artifact of type Problem statement or opportunity + - type: Target business outcomes + description: Input artifact of type Target business outcomes + - type: Budget range or financial constraints + description: Input artifact of type Budget range or financial constraints + - type: Market research data + description: Input artifact of type Market research data + - type: Competitive intelligence + description: Input artifact of type Competitive intelligence + - type: Stakeholder requirements + description: Input artifact of type Stakeholder requirements + produces: + - type: 'business-case: Comprehensive business justification with financial analysis, + ROI model, risk assessment, and recommendation' + description: 'Output artifact of type business-case: Comprehensive business justification + with financial analysis, ROI model, risk assessment, and recommendation' + - type: 'portfolio-roadmap: Strategic multi-initiative roadmap with timeline, dependencies, + and resource allocation' + description: 'Output artifact of type portfolio-roadmap: Strategic multi-initiative + roadmap with timeline, dependencies, and resource allocation' + - type: 'market-analysis: Market opportunity assessment with sizing, trends, and + target segments' + description: 'Output artifact of type market-analysis: Market opportunity assessment + with sizing, trends, and target segments' + - type: 'competitive-analysis: Competitive landscape analysis with positioning and + differentiation' + description: 'Output artifact of type competitive-analysis: Competitive landscape + analysis with positioning and differentiation' + - type: 'feasibility-study: Technical and business feasibility assessment' + description: 'Output artifact of type feasibility-study: Technical and business + feasibility assessment' + - type: 'strategic-plan: Multi-year strategic planning document with objectives + and key results' + description: 'Output artifact of type strategic-plan: Multi-year strategic planning + document with objectives and key results' + - type: 'value-proposition-canvas: Customer value proposition and fit analysis' + description: 'Output artifact of type value-proposition-canvas: Customer value + proposition and fit analysis' + - type: 'roi-model: Financial return on investment model with multi-year projections' + description: 'Output artifact of type roi-model: Financial return on investment + model with multi-year projections' diff --git a/agents/test.engineer/README.md b/agents/test.engineer/README.md new file mode 100644 index 0000000..4d42b5e --- /dev/null +++ b/agents/test.engineer/README.md @@ -0,0 +1,74 @@ +# Test.Engineer Agent + +## Purpose + +Create comprehensive testing artifacts including test plans, test cases, test results, test automation strategies, and quality assurance reports. Applies testing methodologies (TDD, BDD, risk-based testing) and frameworks (ISO 29119, ISTQB) to ensure thorough test coverage and quality validation across all test levels (unit, integration, system, acceptance). + +## Skills + +This agent uses the following skills: + + +## Artifact Flow + +### Consumes + +- `Requirements or user stories` +- `System architecture or design` +- `Test scope and objectives` +- `Quality criteria and acceptance thresholds` +- `Testing constraints` +- `Defects or test results` + +### Produces + +- `test-plan: Comprehensive test strategy with scope, approach, resources, and schedule` +- `test-cases: Detailed test cases with steps, data, and expected results` +- `test-results: Test execution results with pass/fail status and defect tracking` +- `test-automation-strategy: Test automation framework and tool selection` +- `acceptance-criteria: User story acceptance criteria in Given-When-Then format` +- `performance-test-plan: Performance and load testing strategy` +- `integration-test-plan: Integration testing approach with interface validation` +- `regression-test-suite: Regression test suite for continuous integration` +- `quality-assurance-report: QA summary with metrics, defects, and quality assessment` + +## Example Use Cases + +- Test scope (functional, security, accessibility, performance) +- Test levels (unit, integration, system, UAT) +- Test approach by feature area +- Platform coverage (iOS 14+, Android 10+) +- Test environment and data requirements +- Accessibility testing (WCAG 2.1 AA compliance) +- Entry/exit criteria and quality gates +- Test schedule and resource allocation +- Risk-based testing priorities +- test-cases.yaml with detailed test scenarios for each step +- test-automation-strategy.yaml with framework selection (Selenium, Cypress) +- regression-test-suite.yaml for CI/CD integration +- Test cases include: happy path, error handling, edge cases +- Automation coverage: 80% of critical user journeys +- Performance requirements (throughput, latency, concurrency) +- Load testing scenarios (baseline, peak, stress, soak) +- Performance metrics and SLAs +- Test data and environment sizing +- Monitoring and observability requirements +- Performance acceptance criteria + +## Usage + +```bash +# Activate the agent +/agent test.engineer + +# Or invoke directly +betty agent run test.engineer --input +``` + +## Created By + +This agent was created by **meta.agent**, the meta-agent for creating agents. + +--- + +*Part of the Betty Framework* diff --git a/agents/test.engineer/agent.yaml b/agents/test.engineer/agent.yaml new file mode 100644 index 0000000..8f328ae --- /dev/null +++ b/agents/test.engineer/agent.yaml @@ -0,0 +1,65 @@ +name: test.engineer +version: 0.1.0 +description: Create comprehensive testing artifacts including test plans, test cases, + test results, test automation strategies, and quality assurance reports. Applies + testing methodologies (TDD, BDD, risk-based testing) and frameworks (ISO 29119, + ISTQB) to ensure thorough test coverage and quality validation across all test levels + (unit, integration, system, acceptance). +status: draft +reasoning_mode: iterative +capabilities: + - Develop comprehensive test strategies across multiple levels and techniques + - Produce reusable automation assets and coverage reporting + - Analyze defect data to recommend quality improvements +skills_available: + - artifact.create + - artifact.validate + - artifact.review +permissions: + - filesystem:read + - filesystem:write +artifact_metadata: + consumes: + - type: Requirements or user stories + description: Input artifact of type Requirements or user stories + - type: System architecture or design + description: Input artifact of type System architecture or design + - type: Test scope and objectives + description: Input artifact of type Test scope and objectives + - type: Quality criteria and acceptance thresholds + description: Input artifact of type Quality criteria and acceptance thresholds + - type: Testing constraints + description: Input artifact of type Testing constraints + - type: Defects or test results + description: Input artifact of type Defects or test results + produces: + - type: 'test-plan: Comprehensive test strategy with scope, approach, resources, + and schedule' + description: 'Output artifact of type test-plan: Comprehensive test strategy with + scope, approach, resources, and schedule' + - type: 'test-cases: Detailed test cases with steps, data, and expected results' + description: 'Output artifact of type test-cases: Detailed test cases with steps, + data, and expected results' + - type: 'test-results: Test execution results with pass/fail status and defect tracking' + description: 'Output artifact of type test-results: Test execution results with + pass/fail status and defect tracking' + - type: 'test-automation-strategy: Test automation framework and tool selection' + description: 'Output artifact of type test-automation-strategy: Test automation + framework and tool selection' + - type: 'acceptance-criteria: User story acceptance criteria in Given-When-Then + format' + description: 'Output artifact of type acceptance-criteria: User story acceptance + criteria in Given-When-Then format' + - type: 'performance-test-plan: Performance and load testing strategy' + description: 'Output artifact of type performance-test-plan: Performance and load + testing strategy' + - type: 'integration-test-plan: Integration testing approach with interface validation' + description: 'Output artifact of type integration-test-plan: Integration testing + approach with interface validation' + - type: 'regression-test-suite: Regression test suite for continuous integration' + description: 'Output artifact of type regression-test-suite: Regression test suite + for continuous integration' + - type: 'quality-assurance-report: QA summary with metrics, defects, and quality + assessment' + description: 'Output artifact of type quality-assurance-report: QA summary with + metrics, defects, and quality assessment' diff --git a/agents/testing.orchestrator/README.md b/agents/testing.orchestrator/README.md new file mode 100644 index 0000000..c89cefc --- /dev/null +++ b/agents/testing.orchestrator/README.md @@ -0,0 +1,42 @@ +# Testing.Orchestrator Agent + +Orchestrates testing workflows across unit, integration, and end-to-end tests + +## Purpose + +This orchestrator agent coordinates complex testing workflows by composing and sequencing multiple skills. It handles the complete lifecycle from planning through execution and validation. + +## Capabilities + +- Coordinate test planning and design +- Manage test execution and reporting +- Orchestrate quality assurance workflows +- Handle test data generation and management +- Coordinate continuous testing pipelines + +## Available Skills + +- `test.example` +- `workflow.validate` +- `api.test` + +## Usage + +This agent uses iterative reasoning to: +1. Analyze requirements +2. Plan execution steps +3. Coordinate skill execution +4. Validate results +5. Handle errors and retries + +## Status + +**Generated**: Auto-generated from taxonomy gap analysis + +## Next Steps + +- [ ] Review and refine capabilities +- [ ] Test with real workflows +- [ ] Add domain-specific examples +- [ ] Integrate with existing agents +- [ ] Document best practices diff --git a/agents/testing.orchestrator/agent.yaml b/agents/testing.orchestrator/agent.yaml new file mode 100644 index 0000000..a09ea93 --- /dev/null +++ b/agents/testing.orchestrator/agent.yaml @@ -0,0 +1,52 @@ +name: testing.orchestrator +version: 0.1.0 +description: Orchestrates testing workflows across unit, integration, and end-to-end + tests +capabilities: +- Coordinate test planning and design +- Manage test execution and reporting +- Orchestrate quality assurance workflows +- Handle test data generation and management +- Coordinate continuous testing pipelines +skills_available: +- test.example +- workflow.validate +- api.test +reasoning_mode: iterative +tags: +- testing +- orchestration +- qa +- quality +workflow_pattern: '1. Analyze incoming request and requirements + + 2. Identify relevant testing skills and workflows + + 3. Compose multi-step execution plan + + 4. Execute skills in coordinated sequence + + 5. Validate intermediate results + + 6. Handle errors and retry as needed + + 7. Return comprehensive results' +example_task: "Input: \"Complete testing workflow from start to finish\"\n\nAgent\ + \ will:\n1. Break down the task into stages\n2. Select appropriate skills for each\ + \ stage\n3. Execute create \u2192 validate \u2192 review \u2192 publish lifecycle\n\ + 4. Monitor progress and handle failures\n5. Generate comprehensive reports" +error_handling: + timeout_seconds: 300 + retry_strategy: exponential_backoff + max_retries: 3 +output: + success: + - Testing workflow results + - Execution logs and metrics + - Validation reports + - Generated artifacts + failure: + - Error details and stack traces + - Partial results (if available) + - Remediation suggestions +status: generated diff --git a/commands/meta-config-router.yaml b/commands/meta-config-router.yaml new file mode 100644 index 0000000..8c655cd --- /dev/null +++ b/commands/meta-config-router.yaml @@ -0,0 +1,81 @@ +name: meta-config-router +version: 0.1.0 +description: "Configure Claude Code Router for multi-model LLM support (OpenAI, Claude, Ollama, etc.)" +status: active + +execution: + type: agent + target: meta.config.router + context: + mode: oneshot + +parameters: + - name: routing_config_path + type: string + required: true + description: "Path to YAML or JSON input file containing router configuration" + + - name: apply_config + type: boolean + required: false + default: false + description: "Write configuration to ~/.claude-code-router/config.json (default: false)" + + - name: output_mode + type: string + required: false + default: "preview" + enum: + - preview + - file + - both + description: "Output mode: preview (show only), file (write only), or both (default: preview)" + +permissions: + - filesystem:read + - filesystem:write + +tags: + - llm + - router + - configuration + - meta + - infra + - openrouter + - claude + - ollama + - multi-model + +artifact_metadata: + consumes: + - type: router-config-input + description: Router configuration input file (YAML or JSON) + file_pattern: "*-router-input.{json,yaml,yml}" + content_type: application/yaml + + produces: + - type: llm-router-config + description: Complete Claude Code Router configuration + file_pattern: "config.json" + content_type: application/json + + - type: audit-log-entry + description: Audit trail entry for configuration events + file_pattern: "audit_log.json" + content_type: application/json + +examples: + - name: Preview configuration (no file write) + command: /meta-config-router --routing_config_path=examples/router-config.yaml + + - name: Apply configuration to disk + command: /meta-config-router --routing_config_path=examples/router-config.yaml --apply_config + + - name: Both preview and write + command: /meta-config-router --routing_config_path=examples/router-config.yaml --apply_config --output_mode=both + +notes: + - "API keys can use environment variable substitution (e.g., ${OPENROUTER_API_KEY})" + - "Local providers (localhost/127.0.0.1) don't require API keys" + - "All configuration changes are audited for traceability" + - "Preview mode allows verification before applying changes" diff --git a/commands/optimize-build.yaml b/commands/optimize-build.yaml new file mode 100644 index 0000000..d839473 --- /dev/null +++ b/commands/optimize-build.yaml @@ -0,0 +1,25 @@ +name: /optimize-build +version: 0.1.0 +description: Optimize build processes and speed +parameters: +- name: project_path + type: string + description: Path to project root directory + required: false + default: . +- name: format + type: enum + description: Output format + required: false + default: human + values: + - human + - json +execution: + type: skill + target: build.optimize +status: active +tags: +- build +- optimization +- performance diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..80888e5 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,1225 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:epieczko/betty:.", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "b8c238ffebff57ddee4d470d9d09907714f86c5c", + "treeHash": "50ade598eb86779431ad917f9a279dd84e2cd3006b65c97df05563b9e16bac33", + "generatedAt": "2025-11-28T10:16:48.686649Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "betty-framework", + "description": "Betty Framework is RiskExec's system for structured, auditable AI-assisted engineering.\nWhere Claude Code provides the runtime, Betty adds methodology, orchestration, and governance—\nturning raw agent capability into a repeatable, enterprise-grade engineering discipline.\n", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "9cebe184a406b6e5eb9f2a044c6d3406bc8900d53966412adc90a8b5ed4391d3" + }, + { + "path": "agents/README.md", + "sha256": "73ccf122113326df9811c3f4349c72bd37e5ec4f7128ea54471dfad5875efdc6" + }, + { + "path": "agents/deployment.engineer/agent.yaml", + "sha256": "70d0750e311805acb451231ef3f9c7f23c2551872d8a53f5cbcac129065008f6" + }, + { + "path": "agents/deployment.engineer/README.md", + "sha256": "36f1e284f4be5d0e16a910406c2d068fdedcec404f262d21c85cebbf625819bf" + }, + { + "path": "agents/api.designer/agent.yaml", + "sha256": "765df1f7f19e18053f6aef8ec64b1107433bdd1ee8894812a5250d0c25d5448d" + }, + { + "path": "agents/api.designer/README.md", + "sha256": "69069abc914da34c67da74ec47b5eaa6595107d11e471dbb941d1543d03c2258" + }, + { + "path": "agents/file.processor/agent.yaml", + "sha256": "2c0b62eeb862ab00b36a92ecea0bbac7fe422cb8874d0ad3bc4ca3244ede7197" + }, + { + "path": "agents/file.processor/README.md", + "sha256": "d52d4660c8cc5e847da266915083bf617d3375600970cc9d38e04a1f1ce6fbb1" + }, + { + "path": "agents/data.orchestrator/agent.yaml", + "sha256": "a4fb1552d5f19ca1cbd81dfd0cf57ba8dd52566fc1ad7215e7fa6b9425d37d17" + }, + { + "path": "agents/data.orchestrator/README.md", + "sha256": "089bc1b3a1114e6ee5bc6ff316b740996b13cbfb24dcd72dceb63a573857e4f2" + }, + { + "path": "agents/meta.create/agent.yaml", + "sha256": "b76e6fc18efbf9fcffba45d97a9efd3f7dce205adb25fed41f34c13582839c77" + }, + { + "path": "agents/meta.create/README.md", + "sha256": "3e6700f6be18b7061ea70cbb03c3b3b1fce077465b383ed8d7541d293de36b9a" + }, + { + "path": "agents/meta.create/meta_create.py", + "sha256": "13602be16ea0727b201d4c5b017b6290d1e17b459904fc721bae2d3e8c3bccbb" + }, + { + "path": "agents/ai.orchestrator/agent.yaml", + "sha256": "9c4a15351ce062f5ba43bfbd938eb9e82e8ccef79c548385ddf4fde27fc81b99" + }, + { + "path": "agents/ai.orchestrator/README.md", + "sha256": "62c01a4151d004e55bb65a97b447d2de3551fe67cfc62a552e2d582d44ee9700" + }, + { + "path": "agents/meta.command/agent.yaml", + "sha256": "71ab88918598116b3ddb5221e47f755ca92a3e3231d6623189bfdd2950cc2167" + }, + { + "path": "agents/meta.command/meta_command.py", + "sha256": "afc9ab84465ed87e9c86914620984045473bae72f74b097190b37a4085607180" + }, + { + "path": "agents/meta.command/README.md", + "sha256": "78e434863f2a7cee70be2daf6502556c32a5d05c47832a5b982d0e335f4040ea" + }, + { + "path": "agents/strategy.architect/agent.yaml", + "sha256": "004c3ef1ca02d72d47f2387ba97fedde9584f68f0d1eeed6f070c890fad321f1" + }, + { + "path": "agents/strategy.architect/README.md", + "sha256": "3b8ccb4889276faf706c384d98bc97135db116978ba73eddc43c7a7fc9d79a1f" + }, + { + "path": "agents/testing.orchestrator/agent.yaml", + "sha256": "1c16785dc7eaec9a384d56d9e4e3bdde7b3b714ecf94eb304616b98703cb4e5d" + }, + { + "path": "agents/testing.orchestrator/README.md", + "sha256": "fcb1f298c55979947e03591314eb5d7bc1c362f86656281b86d2eec4576e5064" + }, + { + "path": "agents/data.validator/agent.yaml", + "sha256": "37b74b833df31c1c2861412f28662191248182edbd3bd76bbc503b89a8da72b9" + }, + { + "path": "agents/data.validator/README.md", + "sha256": "d7dcf224f63e7814be5f7583fca6a196520d5a6917d2511630914bbc5d75ed38" + }, + { + "path": "agents/api.analyzer/agent.yaml", + "sha256": "5f88528bcf74f2dbb7bbcfc5c6329a82e168417117fb04a05b5787e7048895f7" + }, + { + "path": "agents/api.analyzer/README.md", + "sha256": "c47dc7d09f2279f83ee81632d093edb7ddb6d8f9c256ded0be551aa201ab4dec" + }, + { + "path": "agents/api.orchestrator/agent.yaml", + "sha256": "edeeecb46ac4772add82e0a708ecf994886e26490a1b9df3c7637eefd2479374" + }, + { + "path": "agents/api.orchestrator/README.md", + "sha256": "b87512f9bde691157ba82edde4dcea352a20f6c7f1216b1870e0f9332e524def" + }, + { + "path": "agents/meta.hook/agent.yaml", + "sha256": "3e770482874513f0c1968af7b1f4975fb1c8012c93486fa4732768f32050b10e" + }, + { + "path": "agents/meta.hook/meta_hook.py", + "sha256": "27333053b877e1c5f75e3983a05e33f4b28d535bbda5c32f4badc8c8aff14e0a" + }, + { + "path": "agents/meta.hook/README.md", + "sha256": "6e22089fc8a049d79987a6553adcd9eaf2675410efdbeb723f87d34260dc418b" + }, + { + "path": "agents/meta.agent/meta_agent.py", + "sha256": "b15b0d82b40adb86b1d96779d896b4b10e408242ef5437d514977eeabea798fe" + }, + { + "path": "agents/meta.agent/agent.yaml", + "sha256": "8f16fb77fcf585728ae488067a6f543563e38082ccec110aec98aa7db94c826b" + }, + { + "path": "agents/meta.agent/README.md", + "sha256": "88acf3644227de69634cf8a86700cb1bde53defeb00e37f858feefb1b59a69e3" + }, + { + "path": "agents/meta.config.router/meta_config_router.py", + "sha256": "ae801ecbf358609b38e22c36c0584667c26b6f6652e9787709edd4dddb17af7d" + }, + { + "path": "agents/meta.config.router/agent.yaml", + "sha256": "e49e8495ec99cb2f81c1fc8153abf6e217ad3fe9fbeef3a956f5932188dc2d13" + }, + { + "path": "agents/meta.config.router/README.md", + "sha256": "a61c9822879ffbe53ce43b89b892318fe53d961b922707b11455772306e7b9e5" + }, + { + "path": "agents/security.orchestrator/agent.yaml", + "sha256": "dabe3cdbbf3344f4ef3efabf2a9bac778498eeaea1a34bab2c00cf41a126800c" + }, + { + "path": "agents/security.orchestrator/README.md", + "sha256": "9c2a9a3362bd6470e38dceeaf12abe4c68584f7806c80759e512bbc6d5defa3b" + }, + { + "path": "agents/meta.compatibility/agent.yaml", + "sha256": "0358ca2009ba0e5d50d65c133a2c39d3d7f05bdce41a3531bc35502b6618a4d3" + }, + { + "path": "agents/meta.compatibility/meta_compatibility.py", + "sha256": "cce6d92b87eb883c851122599181231013038fa30d750ffb5e7cad84d0f587a3" + }, + { + "path": "agents/meta.compatibility/README.md", + "sha256": "34ecd2c8adeb8830d30eebeb282e53713639abe9cc327f1757786d404b620471" + }, + { + "path": "agents/operations.orchestrator/agent.yaml", + "sha256": "7a57359310db00809c15b28259ace6cb77b3f8f603ef25f36de7acb9885b39ea" + }, + { + "path": "agents/operations.orchestrator/README.md", + "sha256": "e6588dafa13e37e1020faf2e9db54334e9548593939292c022fcb2919559c291" + }, + { + "path": "agents/test.engineer/agent.yaml", + "sha256": "8f8417e740b7806c08325a11693de1f1641e791cf055e6276af2bb0bab24ee4c" + }, + { + "path": "agents/test.engineer/README.md", + "sha256": "ccee4534c79d2a6190690ad4376cf76066307a8f27b469e3adba819a8d9e5fa4" + }, + { + "path": "agents/api.architect/agent.yaml", + "sha256": "a46b0795d97af99a467d48785fd8adebfb47c658c4338a011490d7fc223f70bb" + }, + { + "path": "agents/api.architect/README.md", + "sha256": "9efb749502cc17f12e7ac13e33a3b1b7b8fa428353c5f45f51f13e1bb5122550" + }, + { + "path": "agents/security.architect/agent.yaml", + "sha256": "59a6accaa774afac39719f1c74f8d5137ddd2d5d4d79c297bbca5432f6c77c81" + }, + { + "path": "agents/security.architect/README.md", + "sha256": "b8b111d1eeae005b6e0775a6d896672ada92921ebc669dc381e7aadb2b80ade0" + }, + { + "path": "agents/data.architect/agent.yaml", + "sha256": "45386e69d0f936f5accdac3853919b5a924c4dda91d0843172d108f71287b7b2" + }, + { + "path": "agents/data.architect/README.md", + "sha256": "52bb5743d25290f492af7e61baa94f0b0259a27ff0896fed42c8d2221fa53909" + }, + { + "path": "agents/meta.skill/agent.yaml", + "sha256": "f4432168d02f60a9f4305bf82cf5f45cfb31ffe87380de79687f70b23151347d" + }, + { + "path": "agents/meta.skill/README.md", + "sha256": "2897b902c415c9abd2b0b50f5b7837c06047aa1c5f13d2c167af54d318443b65" + }, + { + "path": "agents/meta.skill/meta_skill.py", + "sha256": "f8eba6e470178166f37a774ef876dc849bfb745cb449f7bb048175ff95188907" + }, + { + "path": "agents/governance.manager/agent.yaml", + "sha256": "3b27b3edb82288e6d0e6b99f082ee5093b1cb0332849c1e9855ec3da56e55ad2" + }, + { + "path": "agents/governance.manager/README.md", + "sha256": "e6c884855692202d9fd9a04336d9a5b97c8f180b727ad33b9bae584432aece31" + }, + { + "path": "agents/code.reviewer/agent.yaml", + "sha256": "a0f5e133f9c48dfae4d0cd3edd4517d81f237df784f20d7411949025302bb05e" + }, + { + "path": "agents/code.reviewer/README.md", + "sha256": "5a791608f108056b3764560c26c3108d40cc7a2c1766f7ebae5a49ab0e347366" + }, + { + "path": "agents/meta.suggest/agent.yaml", + "sha256": "6b64440b8ad7c0eac5add9ccf55b659cb3d08b47d7edc8ef604ac164198d7a43" + }, + { + "path": "agents/meta.suggest/README.md", + "sha256": "2a8acbbcf58b6a9422ac4bd00995e9c6608bbce28f6843cb5504021ce77e425d" + }, + { + "path": "agents/meta.suggest/meta_suggest.py", + "sha256": "35b47cc4fedb8b30d1a209675d4e6375d6c1a7953f420e22c12a8db416232f18" + }, + { + "path": "agents/meta.artifact/agent.yaml", + "sha256": "9df5ae71386700eb7421c86a3cf540b623fe7d8466e22a66ad11b2581589d70c" + }, + { + "path": "agents/meta.artifact/README.md", + "sha256": "c05fb243d97975c520fe95c83333b57f0132b2a3430845d7d10d6c0f4e77a060" + }, + { + "path": "agents/meta.artifact/meta_artifact.py", + "sha256": "43bf33581011f7c5e77c9bc10bdfabdafe9a3980ee1c5322f77f6791cc0d84a5" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "19114d57504c3707f022ee091f236193d0e2da3b4c4963c5e331aa963fd08811" + }, + { + "path": "commands/meta-config-router.yaml", + "sha256": "7b71f9aa9076e27695f0f457b11af37bd19edac92ce148fafa7bc70dc8fa77d2" + }, + { + "path": "commands/optimize-build.yaml", + "sha256": "c3c84eb77a225162910a985e984fc26dc43edc59d55cd995d8e5e3e16af5ed26" + }, + { + "path": "skills/__init__.py", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "skills/README.md", + "sha256": "0e84816d4825a3743ee36c09c0e4864023db45ce18d952b18f7ebb4811f61a0f" + }, + { + "path": "skills/api.define/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/api.define/SKILL.md", + "sha256": "589bee4f3918b00b008dfa13677a9eea150816c62292ce9dc1677d2f6673d09c" + }, + { + "path": "skills/api.define/api_define.py", + "sha256": "66e2d87b1f5ae7d2075bc4484c8ce8c905cf9afd3dadc6121c19f27a7af2fa27" + }, + { + "path": "skills/api.define/skill.yaml", + "sha256": "519f73b72cf9bc12a07c1aa58e7d41e4b389c37b6078e540c66d10ea238811e3" + }, + { + "path": "skills/api.define/templates/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/api.define/templates/openapi-zalando.yaml", + "sha256": "3826d47cd9b77e38bc5a3bf7f6c4da55042a33dbabce7983008bd2b144c020fd" + }, + { + "path": "skills/generate.marketplace/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/generate.marketplace/generate_marketplace.py", + "sha256": "6556a81f4d6cf1b94ad219ae3eaec6f43863f29a07375596ce33315b94a08c4e" + }, + { + "path": "skills/generate.marketplace/SKILL.md", + "sha256": "8e563d23e85f20d8edcbd4c9bd739f488f5f2276e0e149349ccb1b3674893c06" + }, + { + "path": "skills/generate.marketplace/skill.yaml", + "sha256": "05f1642e4e8e2f308d3ca02d89cad5435b0b39cfd947ee697c5aaf1fa70b0975" + }, + { + "path": "skills/code.format/code_format.py", + "sha256": "44714aa94a18ca67120bf928aa3622d8ef1d10ed1992c08af46bcff27c7ffeae" + }, + { + "path": "skills/code.format/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/code.format/test_code_format.py", + "sha256": "50896563c0c0cb739d035cd3fd548caf74ce44471589ae0cb195c404f8731094" + }, + { + "path": "skills/code.format/SKILL.md", + "sha256": "05d937f7466aa455b02ad203f4916d381dab0379bc024eb4eb05a2d22f6febde" + }, + { + "path": "skills/code.format/skill.yaml", + "sha256": "b6a9d062285be939b5ce081d22bfac43580819e01a0687a05bc5b201a8d2054e" + }, + { + "path": "skills/registry.query/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/registry.query/registry_query.py", + "sha256": "162e62ad6c6234ed3a881fcdc50159532d2df93e5639bf2b35189a5bcd80d731" + }, + { + "path": "skills/registry.query/SKILL.md", + "sha256": "2b1cf75cb8c5c492b94325aa7b57debf81e1b5b27eace8635fed554d9fe80da5" + }, + { + "path": "skills/registry.query/skill.yaml", + "sha256": "8af2219960998f3d4c04806beb326b755e22784517a61c4eed05889b7420895f" + }, + { + "path": "skills/workflow.compose/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/workflow.compose/workflow_compose.py", + "sha256": "1dc2556fb80d7f7b7e5d4e3d207fcf0e206446217309ffec50c568bc722843e4" + }, + { + "path": "skills/workflow.compose/SKILL.md", + "sha256": "095336d1c3b327a0eb8d91c9c55af3a14593299a2fb36fb1e5c3322f990fedca" + }, + { + "path": "skills/workflow.compose/skill.yaml", + "sha256": "ad618dd5babe9b53f17598e477399191383e65b4d26c81d1c231642d73025bef" + }, + { + "path": "skills/api.generatemodels/modelina_generate.py", + "sha256": "98c62ffe5166cf4bda0a26a729b50ad29e46a88589eb9a0134e0e00a287e7615" + }, + { + "path": "skills/api.generatemodels/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/api.generatemodels/SKILL.md", + "sha256": "53803407a5320d29f945ef39cdbf2ee1cc48bced6c411b9ed49ba0d71aab6064" + }, + { + "path": "skills/api.generatemodels/skill.yaml", + "sha256": "fbd210f70345f8866be4856710a38923a00cf13f52de9b7b00d341d8fd5ff2c2" + }, + { + "path": "skills/skill.define/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/skill.define/skill_define.py", + "sha256": "dc7d9fad0e3401189548f7c866ebe48cb10c6835f2e370fb5505b85cddf3bb5b" + }, + { + "path": "skills/skill.define/SKILL.md", + "sha256": "798f00e21cef78ddb0537497aeaa179713e1edc059eb84dda36fa6249d09ab99" + }, + { + "path": "skills/skill.define/skill.yaml", + "sha256": "78dea08d69e141737926b06d579349d065f7a1453146ade27b9d4eb1e856953c" + }, + { + "path": "skills/docs.lint.links/docs_link_lint.py", + "sha256": "6a457580be184130be1c48911e70de7efeb6a65d869e7fe969e42223d1db3b2a" + }, + { + "path": "skills/docs.lint.links/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/docs.lint.links/SKILL.md", + "sha256": "0cfa5def97a6f15aa8273a3b1528cf8a5769b3282339cebb119077365f044cd0" + }, + { + "path": "skills/docs.lint.links/skill.yaml", + "sha256": "bb41ae757cd0e969801b5f55b65b158aaafa21dd5b2e92f21a1e37724c202a14" + }, + { + "path": "skills/command.define/command_define.py", + "sha256": "511cd4cf52673e7fdbf484a1684dc0f4e184992313a8f1c60fb3d6cdd68a8707" + }, + { + "path": "skills/command.define/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/command.define/SKILL.md", + "sha256": "5f3f1fd68fadfa16b8455c582923528add0bc38648cc1605090b8d45fc7b7a82" + }, + { + "path": "skills/command.define/skill.yaml", + "sha256": "b3c76b2a4539c01438182bf582a552021071be6c2c5d2a60b4ab24db17d0135e" + }, + { + "path": "skills/epic.decompose/test_epic_decompose.py", + "sha256": "4206a9c3bb80d18972bd4911c0f39ed7525151efca45a12744c01efb73b664b4" + }, + { + "path": "skills/epic.decompose/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/epic.decompose/SKILL.md", + "sha256": "ce1f011c4ca2135d0ef71dc08ccb303ad8385122552c215a778970b703dd5431" + }, + { + "path": "skills/epic.decompose/epic_decompose.py", + "sha256": "c679193cbaf8caacdf51cfe8526e22f58ac1f957949e144ebf003ba4f90a2e24" + }, + { + "path": "skills/epic.decompose/skill.yaml", + "sha256": "c22e758136602e03f001558ef8497bf8a4a3bd38620548f729f2781c0f569c0b" + }, + { + "path": "skills/artifact.validate/artifact_validate.py", + "sha256": "c0c0c6517cbeca6b9880e9cabd25b6c108694541ad1614bb6b816ccbeb424f30" + }, + { + "path": "skills/artifact.validate/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/artifact.validate/README.md", + "sha256": "e4aade19b29bfb3a3cc533b4bf07ad83b86d420c83c577d2654a4d45e9338c41" + }, + { + "path": "skills/artifact.validate/skill.yaml", + "sha256": "f0c4b9978261c5118184fddfee8cf1ba1669a87e908af33c7e7b8c41ee186d57" + }, + { + "path": "skills/plugin.sync/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/plugin.sync/SKILL.md", + "sha256": "fa40415d5fa0df1476bcfb97d4d144bf490d91ebc5bf58268e42ae8df26869ab" + }, + { + "path": "skills/plugin.sync/plugin_sync.py", + "sha256": "4ffbf148d07f40766e86901b6926a40fdb7ba3f7c713b13e63247f04492a6cfb" + }, + { + "path": "skills/plugin.sync/skill.yaml", + "sha256": "0a4580a8a1513f7aec51c3456fbc1abc141bd8f32652fee20afcb14e097c7193" + }, + { + "path": "skills/git.cleanupbranches/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/git.cleanupbranches/README.md", + "sha256": "0c632bfebbc0fc9f2fc00e4e51b1336f467d3fc1b112923b9d2e5ff9982c6d1e" + }, + { + "path": "skills/git.cleanupbranches/git_cleanupbranches.py", + "sha256": "314129197ac5355b006ed3b3c7c1eb24e9f2b7eee42a58150335bfd5e0761c98" + }, + { + "path": "skills/git.cleanupbranches/test_git_cleanupbranches.py", + "sha256": "cb0c7d2cdfecc67f0a91d9ba56633cc70b12f8f6ced7d610a8ea0ab18cb5d16e" + }, + { + "path": "skills/git.cleanupbranches/skill.yaml", + "sha256": "ee938ef523d70111662979b5948c38871e3ebfe848c3f2fc0cab44da2c13157a" + }, + { + "path": "skills/hook.register/hook_register.py", + "sha256": "9750acdfe68e7949f7ee465b38bb5712170f6a7739aeae681c9a91eee578f79c" + }, + { + "path": "skills/hook.register/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/hook.register/SKILL.md", + "sha256": "b8da137553c60593cf78da81288b9722001f825e564bcd9ef46142dffbfac613" + }, + { + "path": "skills/hook.register/skill.yaml", + "sha256": "86b56fc98fcf44a3fcd7ec605217b136de17f7de7bb67c9b979063ec571971ee" + }, + { + "path": "skills/docs.sync.pluginmanifest/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/docs.sync.pluginmanifest/plugin_manifest_sync.py", + "sha256": "fa7b7adc73bd641fb31b23f91c02da23317b1833499b46dbe1a8c7696039479d" + }, + { + "path": "skills/docs.sync.pluginmanifest/SKILL.md", + "sha256": "cc62ba73055ef35bb7bb0ed33bc50ddfef53722d003f36b52f06e383dd87a350" + }, + { + "path": "skills/docs.sync.pluginmanifest/skill.yaml", + "sha256": "38fc9590e7d85c54fc9e2b05e81729e814edeb87a268e1f2a449ab61783a4845" + }, + { + "path": "skills/docs.sync.readme/readme_sync.py", + "sha256": "b853b95390f6a2e1bcf84162cc2d895173becdbfc225dabce859ff14288c7900" + }, + { + "path": "skills/docs.sync.readme/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/docs.sync.readme/sync_report.json", + "sha256": "18ef431101dea47dcb856fee0a0c1022b6de68a8bc0ac7a2812d2e62db145f7e" + }, + { + "path": "skills/docs.sync.readme/SKILL.md", + "sha256": "c5112830cddd3600848bc4508afa0d00bf6e900cddf8099c396fe97d6ae9d93e" + }, + { + "path": "skills/docs.sync.readme/skill.yaml", + "sha256": "2ccdd455bd67aaf9d9346e15235f1758264b7da39f771ffda015f84a487fc388" + }, + { + "path": "skills/file.compare/test_file_compare.py", + "sha256": "4b54c5a972862d7e31837ba799bc5149f1c820006033d178404f4d2a486e9c13" + }, + { + "path": "skills/file.compare/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/file.compare/README.md", + "sha256": "adac849a4493dbfd0c8044cc609dce5817864f18d445f272e0cb2ff0a5c47280" + }, + { + "path": "skills/file.compare/skill.yaml", + "sha256": "c314a67c782d38d6f52c551b31b9725f62207b1cc880644841a5eb1eedb2ab21" + }, + { + "path": "skills/file.compare/file_compare.py", + "sha256": "ea00f70c00d112b73b5016f8fb55b7a421033b0b90283707a862fff59c4297db" + }, + { + "path": "skills/docs.expand.glossary/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/docs.expand.glossary/glossary_expand.py", + "sha256": "11a2b77026eb5ced6045d8173111946f5b3259ecf747c0c3312a8cf64a02ccc5" + }, + { + "path": "skills/docs.expand.glossary/SKILL.md", + "sha256": "4e4689a1533478ca632f0322d48b82bc8ef5b5a98acfc83584c15068bac9001c" + }, + { + "path": "skills/docs.expand.glossary/skill.yaml", + "sha256": "855068e1e0b4348b13de3142715e47acfa81756c10e6467c107ab8d0bb8dd464" + }, + { + "path": "skills/threat.model.generate/threat_model_generate.py", + "sha256": "99c6ddf4e9990fe9eb32412f702fb63cc858b94bb4ed485fe621a2924c4b9037" + }, + { + "path": "skills/threat.model.generate/SKILL.md", + "sha256": "c1c9f6d448157063597160c89bd0ddd3dcc49db79fd081321ec3d2d2599fbfe8" + }, + { + "path": "skills/threat.model.generate/skill.yaml", + "sha256": "f876fd1570adb6919aff735549f13cac3cad5a81b77c2a4d412338b46e69865e" + }, + { + "path": "skills/generate.docs/generate_docs.py", + "sha256": "b43ca2e0c7e6755750a47ac6d8d746be668ac02ec1cb4bed5212babbbda9dc8d" + }, + { + "path": "skills/generate.docs/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/generate.docs/SKILL.md", + "sha256": "453bca9469d7783e74fe66bca6c9c56c1c38f39449b38800119aa3e49a5a4906" + }, + { + "path": "skills/generate.docs/skill.yaml", + "sha256": "75de79048e3bcc012b4cbf96df2618399d94d8c15a1c4d6443438c18e76e2f48" + }, + { + "path": "skills/story.write/test_story_write.py", + "sha256": "756d353f5db6a51696d1a2166effb11712b1131a69622bfaecc4b70863a471a3" + }, + { + "path": "skills/story.write/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/story.write/story_write.py", + "sha256": "cee84ec61e197e0e0206bc6259c6018e1fbf912dbd8c2c5529772dd4f06240dc" + }, + { + "path": "skills/story.write/SKILL.md", + "sha256": "ce56eb678d245d833c210575fdb49c16bdd33eb995bfc7e9adeb5871118fe6a9" + }, + { + "path": "skills/story.write/skill.yaml", + "sha256": "c00df6c0efaea81a2da4996382996ab4aaea0f6a37c4678b7b0c608ebd6385ce" + }, + { + "path": "skills/registry.update/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/registry.update/registry_update.py", + "sha256": "539753f1542b5f6d38be7e8a5f0313be61ea7551066437b9dc14a0cebd6119a7" + }, + { + "path": "skills/registry.update/SKILL.md", + "sha256": "cb6cabb5db1d29683e5f6fdc2a5fc092c9273aa7b7aa5b293880e929979b4314" + }, + { + "path": "skills/registry.update/skill.yaml", + "sha256": "441ce14d4bbf3021e3715baf245e493f7ccf564f85fca40aca81110755fdfd22" + }, + { + "path": "skills/artifact.define/registry_loader.py", + "sha256": "35f1aa50f963b2ea62441143da7d2373eed45f0f64f3bee358c4317127a10d7f" + }, + { + "path": "skills/artifact.define/artifact_define.py", + "sha256": "ba1155c706f586bca4c00697f086bbdd5a2b3c0d9652090ce7cc8708fcf3abe9" + }, + { + "path": "skills/artifact.define/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/artifact.define/artifact_define.py.backup", + "sha256": "ee43d7be73a748d5f6b51f9f4a755554be4a89d96d71abf6a73e96a17e587e6d" + }, + { + "path": "skills/artifact.define/skill.yaml", + "sha256": "8bb9b37fdc3523c4ca76720ae3e89b1362c9b8b425c5190a3fad3b1d2576ba30" + }, + { + "path": "skills/registry.validate/registry_validate.py", + "sha256": "6a0ce8e2190c794f98ae8fbf1642ec62492704b665e9161b130a493450d37957" + }, + { + "path": "skills/registry.validate/skill.yaml", + "sha256": "a492297962f831455dab13cdfbd9600059fe205047bbb4167daf1bde07f66908" + }, + { + "path": "skills/agent.compose/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/agent.compose/agent_compose.py", + "sha256": "61a9404eb5fdfaafd93bd7a93c37d084d46e4060282ea984bb81a33bae9cf031" + }, + { + "path": "skills/agent.compose/skill.yaml", + "sha256": "1354d159ec8e4417a850cba7b509e907c171d752b7f22e2af60b5217344e0a78" + }, + { + "path": "skills/hook.simulate/hook_simulate.py", + "sha256": "9ff0fd8883e4d04ee54ae88f3a97f4b40a7b0505b7e83e97705fb41c37e90ae3" + }, + { + "path": "skills/hook.simulate/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/hook.simulate/SKILL.md", + "sha256": "a17a3779fbb1910abaaf13b8301651ec3ede07bcf1abb1d969b32948cc291603" + }, + { + "path": "skills/hook.simulate/skill.yaml", + "sha256": "78a7f4a3184c9fa382b8cfffc6e42b845cb3eef8462f813bb26692b748099dc6" + }, + { + "path": "skills/data.transform/test_data_transform.py", + "sha256": "2579a467ab4fad0d136fea1ba479a8db3198d405131523729b5c25c6decec10a" + }, + { + "path": "skills/data.transform/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/data.transform/README.md", + "sha256": "edb62549c5b380e20020505bb32072dec4990a98a4ab3729269bdc3cbd952cd6" + }, + { + "path": "skills/data.transform/skill.yaml", + "sha256": "d4bd17549ea539eafe6c217d06fd0ddc3a4ffe73c3a3cd737ec20b6810f4f804" + }, + { + "path": "skills/data.transform/data_transform.py", + "sha256": "9e0e2bce6d79cac536fa512150959fccf249cea6bf6198def3eaa53625cc1df4" + }, + { + "path": "skills/agent.run/agent_run.py", + "sha256": "b4914b4f57dbb05117de1af79df85cf37967619bde3edc9aae8c89957b4889e2" + }, + { + "path": "skills/agent.run/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/agent.run/SKILL.md", + "sha256": "82efafa465397be2ce322350889283fc46abadc5671088fe986b6f2fdcfac6f4" + }, + { + "path": "skills/agent.run/skill.yaml", + "sha256": "73189a66913287925c584ec652da8fc83e95ee47d93781bec155b6904a5a748c" + }, + { + "path": "skills/workflow.validate/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/workflow.validate/workflow_validate.py", + "sha256": "e141038ce3f69dae8e5f5f2a09fdcaabf19539f633b37936080b3e3853c541d5" + }, + { + "path": "skills/workflow.validate/SKILL.md", + "sha256": "e5c21e73d5eb69f54b93bb9192cb22547957c441034b380b782a4dcc18923547" + }, + { + "path": "skills/workflow.validate/skill.yaml", + "sha256": "716057479285bc1b9eb08d2f9f3471631321cbb699cc86c5368e98052b284bf9" + }, + { + "path": "skills/api.test/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/api.test/README.md", + "sha256": "30d5d54075b26eb56338c44bfb4b7fb4ed954981c316520148961d10c5651e07" + }, + { + "path": "skills/api.test/test_api_test.py", + "sha256": "f495b61bbd81bd3d6e18ee43434cdaf6a0d2c186ec80e223bc92a572751ca964" + }, + { + "path": "skills/api.test/skill.yaml", + "sha256": "5854c314dd52f08f986f873459a3a7d629a785b41df0f4848b5cb1dd28d60bea" + }, + { + "path": "skills/api.test/api_test.py", + "sha256": "e18eb58e8916282a75ccf648ac092c2a8d3fdaba699227005605bbe0e8392df0" + }, + { + "path": "skills/telemetry.capture/telemetry_utils.py", + "sha256": "7bf08ee7e4eec518eb4fef188dab3fc9eead60263c498251a82def1f2c49597f" + }, + { + "path": "skills/telemetry.capture/INTEGRATION_EXAMPLES.md", + "sha256": "e76ab66827cda697360f561afe0d8241f7f93c39f58c077b9c806e2fda7da0f6" + }, + { + "path": "skills/telemetry.capture/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/telemetry.capture/telemetry_capture.py", + "sha256": "f0a73a1e10fc5c627a829aa23865ea636316ae9f96932ba1f40b8862a5308c77" + }, + { + "path": "skills/telemetry.capture/SKILL.md", + "sha256": "dd39db7b071baffe131312694c9a42c6c71345e6b53ae4e075632ade3ec22500" + }, + { + "path": "skills/telemetry.capture/skill.yaml", + "sha256": "c2d1fe053a7166252d26fb2cdc32c3a18b9202c1501833bb90ed9ed92bad565b" + }, + { + "path": "skills/audit.log/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/audit.log/SKILL.md", + "sha256": "d58bae76efcf1e8afa96e32726e4d8f42132a8745bb1b114bf11a70f21a0f354" + }, + { + "path": "skills/audit.log/skill.yaml", + "sha256": "c3eb3131898ef27aa2c9f8d3f43d154f6922967e8d61f387c57fdc189b8e8c19" + }, + { + "path": "skills/audit.log/audit_log.py", + "sha256": "a9ffed68701d004615525b44206d9eb43833391fec9700301416957df81dceee" + }, + { + "path": "skills/artifact.validate.types/artifact_validate_types.py", + "sha256": "a466dcd0aa96f3494d543552789a2ccb9139a6a2d350115ae0dd135aa5bb6958" + }, + { + "path": "skills/artifact.validate.types/test_artifact_validate_types.py", + "sha256": "09264e869a8c651ee82935b541c6bffdcabd74382c6444c3a5aa791a58be427f" + }, + { + "path": "skills/artifact.validate.types/benchmark_performance.py", + "sha256": "bf170ddd3f77ae2033fd95dff09f388d0e5522d1f4c92f9966efc262cc73b12c" + }, + { + "path": "skills/artifact.validate.types/SKILL.md", + "sha256": "4c00d1e5084c4a822327cbdcdfd2e9001f7e876a94532ec13cc6701cdc4b3513" + }, + { + "path": "skills/artifact.validate.types/benchmark_results.json", + "sha256": "c4c008554afaacd9be4762610907653c9eac5463384489ba78bb7f2718ae2588" + }, + { + "path": "skills/artifact.validate.types/skill.yaml", + "sha256": "072f9badfe10ca01feea5750e6daf3eba3af0a776ccb0213207452002efe82d0" + }, + { + "path": "skills/config.validate.router/validate_router.py", + "sha256": "a81859d0e2985739a7847a47576063839f79f5d8658ba02ffe46edda34bb6b6d" + }, + { + "path": "skills/config.validate.router/skill.yaml", + "sha256": "1448fa1ecb15f992fdbd1898a548226e21f46e9aa25067c0a65dd02675bb6fcc" + }, + { + "path": "skills/plugin.build/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/plugin.build/SKILL.md", + "sha256": "e3cce861ed09784f2e864dc995124b790c85645e94fa24f7e71d839811622ff4" + }, + { + "path": "skills/plugin.build/skill.yaml", + "sha256": "fd7198c1e77abbfc2490241a23699c9b1da1e00f29ad36a0665031f15f98f1a1" + }, + { + "path": "skills/plugin.build/plugin_build.py", + "sha256": "956418e54d31ae5622e2038806f4f2aaf6718e44e81d2eea268e312adba5cdef" + }, + { + "path": "skills/plugin.publish/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/plugin.publish/SKILL.md", + "sha256": "defe9e85e46a21731819ef385a443a9989496f7cf967e1b3b4387c010597050e" + }, + { + "path": "skills/plugin.publish/skill.yaml", + "sha256": "c4115f1c545fa93f9dc91c333edff3c0d5d18d2a90f0433a502fd71ea13f8bf5" + }, + { + "path": "skills/plugin.publish/plugin_publish.py", + "sha256": "4682d222e7771ba0c0692cdce2af85c1809086b1b568ab0ca031ea158b612c3c" + }, + { + "path": "skills/meta.compatibility/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/meta.compatibility/meta_compatibility.py", + "sha256": "4df3c1b38bac57e4bb104d412465cc8590c482b3148c330cd97bf6a340b3ff8a" + }, + { + "path": "skills/meta.compatibility/README.md", + "sha256": "ff715dd36e67e19d192f17322cfd856debb59d4c1f7bf59a2a8ba15d746172c9" + }, + { + "path": "skills/meta.compatibility/skill.yaml", + "sha256": "ec447e1a5ae4ff32a287f9604f9a9c1c85452af791ec968b68e8ec05054a28e7" + }, + { + "path": "skills/docs.validate.skilldocs/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/docs.validate.skilldocs/SKILL.md", + "sha256": "ceb01a88dd832125b1ecec7573b27c38275899f0aa900a69df1160dae5927413" + }, + { + "path": "skills/docs.validate.skilldocs/skill.yaml", + "sha256": "f9735cd28f732b91da7041582be31bb89a76efe83de240c94c056c89a49025e5" + }, + { + "path": "skills/docs.validate.skilldocs/skill_docs_validate.py", + "sha256": "c63706a959b1351f7871a6e8bcf3525b6dc3e746a822de6b029323eafea3e78b" + }, + { + "path": "skills/artifact.create/artifact_create.py", + "sha256": "2617c79179be77838147acf915bf6e5bced73c693780ea64fc6097a72aa5f83d" + }, + { + "path": "skills/artifact.create/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/artifact.create/README.md", + "sha256": "bb1544fda608077a9bedd7af4977940b1c34fb37d0dd11984f46fc2752ba81bf" + }, + { + "path": "skills/artifact.create/skill.yaml", + "sha256": "1e02fd06d3efcb75b422aac9b12a2f73b30b8c2a87f6e9b73d4c9ea153cf9c76" + }, + { + "path": "skills/api.compatibility/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/api.compatibility/check_compatibility.py", + "sha256": "71c445bd539d4ac316bb0d472141680d7695a2b5327b84ddd7e60a961778f894" + }, + { + "path": "skills/api.compatibility/SKILL.md", + "sha256": "956211631bb36b2b750e4dd247e3100577b6b8b931fd989b0b283bfa36e77ff0" + }, + { + "path": "skills/api.compatibility/skill.yaml", + "sha256": "8e82c1c21c517df492b654ebbf71d066b3ed8fda93d69638a12d05bb8382e537" + }, + { + "path": "skills/api.validate/api_validate.py", + "sha256": "f7561a98e89da12db0de911be4933eddcdd4ce7e4c996009c5cf1e502b9cb83b" + }, + { + "path": "skills/api.validate/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/api.validate/SKILL.md", + "sha256": "ed85c59529d2e28030304bee54f18a492d786ec7fd2be41659a1fef984b874be" + }, + { + "path": "skills/api.validate/skill.yaml", + "sha256": "3b324631a62aa77be9962515e4135e71e6213f5f560e4c51d9be8d228f18166d" + }, + { + "path": "skills/api.validate/validators/zalando_rules.py", + "sha256": "ab437fb8a51656ea47ed7b702f9276f97aa26fba6e214a3f6e1a8ce9f926102a" + }, + { + "path": "skills/api.validate/validators/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/test.example/test_test_example.py", + "sha256": "bb1622779e74b5640229a8c76fdeeb97071704938da3921a4a627c3b3190ab56" + }, + { + "path": "skills/test.example/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/test.example/README.md", + "sha256": "ffda32b38ee1cb4a9d538ad0644d6e3a7c14a949dc5f169bf538ffe9cd39047a" + }, + { + "path": "skills/test.example/test_example.py", + "sha256": "c5f3cda48b6331371eae931bcdd405d19f36f1433d9efe86e19f73a348525078" + }, + { + "path": "skills/test.example/skill.yaml", + "sha256": "7f9c3da1af01a1572996fa01222312a3ea849dc966dfda7b857c5594f1895fca" + }, + { + "path": "skills/epic.write/test_epic_write.py", + "sha256": "fb99e0dc2c25484b6829ab16ea5ca8636f2a18600a0e218cf5798c1adbff6866" + }, + { + "path": "skills/epic.write/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/epic.write/epic_write.py", + "sha256": "d11be18239faf43bcc8f9e4d34a6c7131c4892baa1daa98ebfc552e062058b40" + }, + { + "path": "skills/epic.write/SKILL.md", + "sha256": "bc3cddfe30cead8e00cbf3618eaf328ecc822ce80af46c3b0e49e6343701733c" + }, + { + "path": "skills/epic.write/skill.yaml", + "sha256": "5dd6ca8a33734915e63e02a459591a7c45000a05dedf597fffcaea75621cc768" + }, + { + "path": "skills/skill.create/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/skill.create/skill_create.py", + "sha256": "9f19d265ddf5c08a375c51542be2a3ebd48cb3bcf1a3818ad2ede10d5b51f00a" + }, + { + "path": "skills/skill.create/SKILL.md", + "sha256": "d78d9b01c18c9c502b4c96173e4952b3afaa301deb087ee9dcab09f234f62a63" + }, + { + "path": "skills/skill.create/skill.yaml", + "sha256": "3683566a770341f48ca86bc82c6e075743a158ce07b200a6847daa78537c0924" + }, + { + "path": "skills/workflow.orchestrate/workflow_orchestrate.py", + "sha256": "0c73fcc469a8a442086190168efab5c84b5cf7128ae7885b879fb51b6d8d4136" + }, + { + "path": "skills/workflow.orchestrate/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/workflow.orchestrate/README.md", + "sha256": "4151932195ecd1adb3bb77cd7dcb47a0d53c728a0e57a4e2177cfbfe6afdd62e" + }, + { + "path": "skills/workflow.orchestrate/test_workflow_orchestrate.py", + "sha256": "166997f3e31370312778719cadc5b4ea477985c77d67e9a2f1a14bb685bdb165" + }, + { + "path": "skills/workflow.orchestrate/skill.yaml", + "sha256": "f052e719d8b2842f58f94c0a1f4d12f20a18708b48da8d965e59969bb5f65818" + }, + { + "path": "skills/build.optimize/build_optimize.py", + "sha256": "6b001fa7731d80f745d709e4a82acca955ea99735a42d77914b9f9a53820915e" + }, + { + "path": "skills/build.optimize/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/build.optimize/skill.yaml", + "sha256": "b7b83fa3b4d9fb0fc590204cdbae47e19f93116e857ed4c74cd01e095e3dccf4" + }, + { + "path": "skills/artifact.scaffold/artifact_scaffold.py", + "sha256": "e77b1ffb9de3c217c783002cc4230820deb86185f14cd70bbc8974e2c5cb23e2" + }, + { + "path": "skills/artifact.scaffold/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/artifact.scaffold/README.md", + "sha256": "73c1bcb1645d3d3482eee9b4b52c3fd337be96a46b0805c3c0f8815f4f21f031" + }, + { + "path": "skills/artifact.scaffold/skill.yaml", + "sha256": "45f5fa726c2a0ffa9db88a82e38f89d768da032d35f8975c758e9046e92fdf3d" + }, + { + "path": "skills/agent.define/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/agent.define/agent_define.py", + "sha256": "cf3a372f8e5141b86fe7fe4f2a4c34c1edec9dbed04580e193461382a97d4b6c" + }, + { + "path": "skills/agent.define/SKILL.md", + "sha256": "36cd25ffdb88d4c6d9b4a956788ee889c70fabf602167c02032e0f9692c85d82" + }, + { + "path": "skills/agent.define/skill.yaml", + "sha256": "6597380e76ab26d98135104296a332ccdcde0f3d04b24600541e92ab193f9181" + }, + { + "path": "skills/registry.diff/registry_diff.py", + "sha256": "ca5fa64b60215ad59576f8c17c6f81fa9f47bc27ca7f48ca1b915ac2581820ed" + }, + { + "path": "skills/registry.diff/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/registry.diff/SKILL.md", + "sha256": "2c8c476862fecafd2ab6fa2e31ce1b11b917c06472a10316eeb0cfe1befd6650" + }, + { + "path": "skills/registry.diff/skill.yaml", + "sha256": "cc21abc23a3b15916b117c24fdfbbc30901b5b023fb8c10d6cea97db60c608f2" + }, + { + "path": "skills/artifact.review/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/artifact.review/artifact_review.py", + "sha256": "125f4248a5f8d6a66fffd4c5cf5cea6789d10a044e3d10ac400b046523f8de37" + }, + { + "path": "skills/artifact.review/README.md", + "sha256": "3702f70952d0dfdc56155c0b002438d379254cd8be90bead9edc8951e6349a7b" + }, + { + "path": "skills/artifact.review/skill.yaml", + "sha256": "55fce02fb581ae49fb133e684b7580ed4d3c697ee851d9bb5b9e0cb4f75c4642" + }, + { + "path": "skills/policy.enforce/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/policy.enforce/policy_enforce.py", + "sha256": "d87fd33c6dc03c85916da078d29f090bbc828bb20c515e4dec3a299805c294fd" + }, + { + "path": "skills/policy.enforce/SKILL.md", + "sha256": "a263441c68d22289629aa9675656536d1065a8a38c1f9fabe2bafe807b85c931" + }, + { + "path": "skills/policy.enforce/skill.yaml", + "sha256": "64dfef82548d7d0ea45dbc53dc576d7e976c128306af3f337e2fb633a38d6a34" + }, + { + "path": "skills/git.createpr/git_createpr.py", + "sha256": "cfeb0d63fffc27ae29e8e9382353b49a7fbd09100f8e340ffcc45ee761569010" + }, + { + "path": "skills/git.createpr/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/git.createpr/README.md", + "sha256": "fe2b1cc9548013cd87b089a3a9c4f5d5051d3eaf55dd19a0541738944862293d" + }, + { + "path": "skills/git.createpr/skill.yaml", + "sha256": "1f7da835acd69cf7023b0126651a82c8ae75a563d24008503012a6e25e4996e7" + }, + { + "path": "skills/git.createpr/test_git_createpr.py", + "sha256": "964f414da01d54a7021b7f304bdd01380eff0e64c4dffec5d2c709ed3530371e" + }, + { + "path": "skills/hook.define/hook_define.py", + "sha256": "1b43a4f5a8e8a91e71ec645af870bd2ad7565e23126dcce69d203a979ab7a266" + }, + { + "path": "skills/hook.define/__init__.py", + "sha256": "46b6dfb9b1207f976ab30bddac3b407b4f45785dfdc481d7db0108057053c833" + }, + { + "path": "skills/hook.define/SKILL.md", + "sha256": "717d103f769d3d243ec15052a17d2f9fe126a46cc71153bb1d8374816025ed76" + }, + { + "path": "skills/hook.define/skill.yaml", + "sha256": "03aadf57ca8cbe82d1683fafb868485442c17c8ef6ab6e9ea8491e7176816432" + }, + { + "path": "skills/config.generate.router/generate_router.py", + "sha256": "31992237dcbb8a028dc1cf379fcdd30dced36706dc87fc54bbcc4f07861827f2" + }, + { + "path": "skills/config.generate.router/skill.yaml", + "sha256": "36b6210575abdb18b28e438270e04ec9fa932a74b78fbfd88361aa9b3c5a8044" + } + ], + "dirSha256": "50ade598eb86779431ad917f9a279dd84e2cd3006b65c97df05563b9e16bac33" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..bfe2b9f --- /dev/null +++ b/skills/README.md @@ -0,0 +1,312 @@ +# Betty Framework Skills + +## ⚙️ **Integration Note: Claude Code Plugin System** + +**Betty skills are Claude Code plugins.** You do not invoke skills via standalone CLI commands (`betty` or direct Python scripts). Instead: + +- **Claude Code serves as the execution environment** for all skill execution +- Each skill is registered through its `skill.yaml` manifest +- Skills become automatically discoverable and executable through Claude Code's natural language interface +- All routing, validation, and execution is handled by Claude Code via MCP (Model Context Protocol) + +**No separate installation step is needed** beyond plugin registration in your Claude Code environment. + +--- + +This directory contains skill manifests and implementations for the Betty Framework. + +## What are Skills? + +Skills are **atomic, composable building blocks** that execute specific operations. Unlike agents (which orchestrate multiple skills with reasoning) or workflows (which follow fixed sequential steps), skills are: + +- **Atomic** — Each skill does one thing well +- **Composable** — Skills can be combined into complex workflows +- **Auditable** — Every execution is logged with inputs, outputs, and provenance +- **Type-safe** — Inputs and outputs are validated against schemas + +## Directory Structure + +Each skill has its own directory containing: +``` +skills/ +├── / +│ ├── skill.yaml # Skill manifest (required) +│ ├── SKILL.md # Documentation (auto-generated) +│ ├── .py # Implementation handler (required) +│ ├── requirements.txt # Python dependencies (optional) +│ └── tests/ # Skill tests (optional) +│ └── test_skill.py +``` + +## Creating a Skill + +### Using meta.skill (Recommended) + +**Via Claude Code:** +``` +"Use meta.skill to create a custom.processor skill that processes custom data formats, +accepts raw-data and config as inputs, and outputs processed-data" +``` + +**Direct execution (development/testing):** +```bash +cat > /tmp/my_skill.md <<'EOF' +# Name: custom.processor +# Purpose: Process custom data formats +# Inputs: raw-data, config +# Outputs: processed-data +# Dependencies: python-processing-tools +EOF +python agents/meta.skill/meta_skill.py /tmp/my_skill.md +``` + +### Manual Creation + +1. Create skill directory: + ```bash + mkdir -p skills/custom.processor + ``` + +2. Create skill manifest (`skills/custom.processor/skill.yaml`): + ```yaml + name: custom.processor + version: 0.1.0 + description: "Process custom data formats" + + inputs: + - name: raw-data + type: file + description: "Input data file" + required: true + - name: config + type: object + description: "Processing configuration" + required: false + + outputs: + - name: processed-data + type: file + description: "Processed output file" + + dependencies: + - python-processing-tools + + status: draft + ``` + +3. Implement the handler (`skills/custom.processor/custom_processor.py`): + ```python + #!/usr/bin/env python3 + """Custom data processor skill implementation.""" + + import sys + from pathlib import Path + + def main(): + if len(sys.argv) < 2: + print("Usage: custom_processor.py [config]") + sys.exit(1) + + raw_data = Path(sys.argv[1]) + config = sys.argv[2] if len(sys.argv) > 2 else None + + # Your processing logic here + print(f"Processing {raw_data} with config {config}") + + if __name__ == "__main__": + main() + ``` + +4. Validate and register: + + **Via Claude Code:** + ``` + "Use skill.define to validate skills/custom.processor/skill.yaml, + then use registry.update to register it" + ``` + + **Direct execution (development/testing):** + ```bash + python skills/skill.define/skill_define.py skills/custom.processor/skill.yaml + python skills/registry.update/registry_update.py skills/custom.processor/skill.yaml + ``` + +## Skill Manifest Schema + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Unique identifier (e.g., `api.validate`) | +| `version` | string | Semantic version (e.g., `0.1.0`) | +| `description` | string | Human-readable purpose statement | +| `inputs` | array[object] | Input parameters and their types | +| `outputs` | array[object] | Output artifacts and their types | + +### Optional Fields + +| Field | Type | Description | +|-------|------|-------------| +| `status` | enum | `draft`, `active`, `deprecated`, `archived` | +| `dependencies` | array[string] | External tools or libraries required | +| `tags` | array[string] | Categorization tags | +| `examples` | array[object] | Usage examples | +| `error_handling` | object | Error handling strategies | + +## Skill Categories + +### Foundation Skills +- **skill.create** — Generate new skill scaffolding +- **skill.define** — Validate skill manifests +- **registry.update** — Update component registries +- **workflow.compose** — Chain skills into workflows + +### API Development Skills +- **api.define** — Create API specifications +- **api.validate** — Validate specs against guidelines +- **api.generate-models** — Generate type-safe models +- **api.compatibility** — Detect breaking changes + +### Governance Skills +- **audit.log** — Record audit events +- **policy.enforce** — Validate against policies +- **telemetry.capture** — Capture usage metrics +- **registry.query** — Query component registry + +### Infrastructure Skills +- **agent.define** — Validate agent manifests +- **agent.run** — Execute agents +- **plugin.build** — Bundle plugins +- **plugin.sync** — Sync plugin manifests + +### Documentation Skills +- **docs.sync.readme** — Regenerate README files +- **generate.docs** — Auto-generate documentation +- **docs.validate.skill_docs** — Validate documentation completeness + +## Using Skills + +### Via Claude Code (Recommended) + +Simply ask Claude to execute the skill by name: + +``` +"Use api.validate to check specs/user-service.openapi.yaml against Zalando guidelines" + +"Use artifact.create to create a threat-model artifact named payment-system-threats" + +"Use registry.query to find all skills in the api category" +``` + +### Direct Execution (Development/Testing) + +For development and testing, you can invoke skill handlers directly: + +```bash +python skills/api.validate/api_validate.py specs/user-service.openapi.yaml + +python skills/artifact.create/artifact_create.py \ + threat-model \ + "Payment processing system" \ + ./artifacts/threat-model.yaml + +python skills/registry.query/registry_query.py --category api +``` + +## Validation + +All skill manifests are automatically validated for: +- Required fields presence +- Name format (`^[a-z][a-z0-9._-]*$`) +- Version format (semantic versioning) +- Input/output schema correctness +- Dependency declarations + +## Registry + +Validated skills are registered in `/registry/skills.json`: +```json +{ + "registry_version": "1.0.0", + "generated_at": "2025-10-26T00:00:00Z", + "skills": [ + { + "name": "api.validate", + "version": "0.1.0", + "description": "Validate API specs against guidelines", + "inputs": [...], + "outputs": [...], + "status": "active" + } + ] +} +``` + +## Composing Skills into Workflows + +Skills can be chained together using the `workflow.compose` skill: + +**Via Claude Code:** +``` +"Use workflow.compose to create a workflow that: +1. Uses api.define to create a spec +2. Uses api.validate to check it +3. Uses api.generate-models to create TypeScript models" +``` + +**Workflow YAML definition:** +```yaml +name: api-development-workflow +version: 0.1.0 + +steps: + - skill: api.define + inputs: + service_name: "user-service" + outputs: + spec_path: "${OUTPUT_DIR}/user-service.openapi.yaml" + + - skill: api.validate + inputs: + spec_path: "${steps[0].outputs.spec_path}" + outputs: + validation_report: "${OUTPUT_DIR}/validation-report.json" + + - skill: api.generate-models + inputs: + spec_path: "${steps[0].outputs.spec_path}" + language: "typescript" + outputs: + models_dir: "${OUTPUT_DIR}/models/" +``` + +## Testing Skills + +Skills should include comprehensive tests: + +```python +# tests/test_custom_processor.py +import pytest +from skills.custom_processor import custom_processor + +def test_processor_with_valid_input(): + result = custom_processor.process("test-data.json", {"format": "json"}) + assert result.success + assert result.output_path.exists() + +def test_processor_with_invalid_input(): + with pytest.raises(ValueError): + custom_processor.process("nonexistent.json") +``` + +Run tests: +```bash +pytest tests/test_custom_processor.py +``` + +## See Also + +- [Main README](../README.md) — Framework overview +- [Agents README](../agents/README.md) — Skill orchestration +- [Skills Framework](../docs/skills-framework.md) — Complete skill taxonomy +- [Betty Architecture](../docs/betty-architecture.md) — Five-layer architecture diff --git a/skills/__init__.py b/skills/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skills/agent.compose/__init__.py b/skills/agent.compose/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/agent.compose/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/agent.compose/agent_compose.py b/skills/agent.compose/agent_compose.py new file mode 100644 index 0000000..f84c85c --- /dev/null +++ b/skills/agent.compose/agent_compose.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +""" +agent_compose.py - Recommend skills for Betty agents based on purpose + +Analyzes skill artifact metadata to suggest compatible skill combinations. +""" + +import os +import sys +import json +import yaml +from typing import Dict, Any, List, Optional, Set +from pathlib import Path + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +def load_registry() -> Dict[str, Any]: + """Load skills registry.""" + registry_path = os.path.join(BASE_DIR, "registry", "skills.json") + with open(registry_path) as f: + return json.load(f) + + +def extract_artifact_metadata(skill: Dict[str, Any]) -> Dict[str, Any]: + """ + Extract artifact metadata from a skill. + + Returns: + Dict with 'produces' and 'consumes' sets + """ + metadata = skill.get("artifact_metadata", {}) + return { + "produces": set(a.get("type") for a in metadata.get("produces", [])), + "consumes": set(a.get("type") for a in metadata.get("consumes", [])) + } + + +def find_skills_by_artifacts( + registry: Dict[str, Any], + produces: Optional[List[str]] = None, + consumes: Optional[List[str]] = None +) -> List[Dict[str, Any]]: + """ + Find skills that produce or consume specific artifacts. + + Args: + registry: Skills registry + produces: Artifact types to produce + consumes: Artifact types to consume + + Returns: + List of matching skills with metadata + """ + skills = registry.get("skills", []) + matches = [] + + for skill in skills: + if skill.get("status") != "active": + continue + + artifacts = extract_artifact_metadata(skill) + + # Check if skill produces required artifacts + produces_match = not produces or any( + artifact in artifacts["produces"] for artifact in produces + ) + + # Check if skill consumes specified artifacts + consumes_match = not consumes or any( + artifact in artifacts["consumes"] for artifact in consumes + ) + + if produces_match or consumes_match: + matches.append({ + "name": skill["name"], + "description": skill.get("description", ""), + "produces": list(artifacts["produces"]), + "consumes": list(artifacts["consumes"]), + "tags": skill.get("tags", []) + }) + + return matches + + +def find_skills_for_purpose( + registry: Dict[str, Any], + purpose: str, + required_artifacts: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Find skills for agent purpose (alias for recommend_skills_for_purpose). + + Args: + registry: Skills registry (for compatibility, currently unused) + purpose: Description of agent purpose + required_artifacts: Artifact types agent needs to work with + + Returns: + Recommendation result with skills and rationale + """ + return recommend_skills_for_purpose(purpose, required_artifacts) + + +def recommend_skills_for_purpose( + agent_purpose: str, + required_artifacts: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Recommend skills based on agent purpose and required artifacts. + + Args: + agent_purpose: Description of agent purpose + required_artifacts: Artifact types agent needs to work with + + Returns: + Recommendation result with skills and rationale + """ + registry = load_registry() + recommended = [] + rationale = {} + + # Keyword matching for purpose + purpose_lower = agent_purpose.lower() + keywords = { + "api": ["api.define", "api.validate", "api.generate-models", "api.compatibility"], + "workflow": ["workflow.validate", "workflow.compose"], + "hook": ["hook.define"], + "validate": ["api.validate", "workflow.validate"], + "design": ["api.define"], + } + + # Find skills by keywords + matched_by_keyword = set() + for keyword, skill_names in keywords.items(): + if keyword in purpose_lower: + matched_by_keyword.update(skill_names) + + # Find skills by required artifacts + matched_by_artifacts = set() + if required_artifacts: + artifact_skills = find_skills_by_artifacts( + registry, + produces=required_artifacts, + consumes=required_artifacts + ) + matched_by_artifacts.update(s["name"] for s in artifact_skills) + + # Combine matches + all_matches = matched_by_keyword | matched_by_artifacts + + # Build recommendation with rationale + skills = registry.get("skills", []) + for skill in skills: + skill_name = skill.get("name") + + if skill_name in all_matches: + reasons = [] + + if skill_name in matched_by_keyword: + reasons.append(f"Purpose matches skill capabilities") + + artifacts = extract_artifact_metadata(skill) + if required_artifacts: + produces_match = artifacts["produces"] & set(required_artifacts) + consumes_match = artifacts["consumes"] & set(required_artifacts) + + if produces_match: + reasons.append(f"Produces: {', '.join(produces_match)}") + if consumes_match: + reasons.append(f"Consumes: {', '.join(consumes_match)}") + + recommended.append(skill_name) + rationale[skill_name] = { + "description": skill.get("description", ""), + "reasons": reasons, + "produces": list(artifacts["produces"]), + "consumes": list(artifacts["consumes"]) + } + + return { + "recommended_skills": recommended, + "rationale": rationale, + "total_recommended": len(recommended) + } + + +def analyze_artifact_flow(skills_metadata: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Analyze artifact flow between recommended skills. + + Args: + skills_metadata: List of skill metadata + + Returns: + Flow analysis showing how artifacts move between skills + """ + all_produces = set() + all_consumes = set() + flows = [] + + for skill in skills_metadata: + produces = set(skill.get("produces", [])) + consumes = set(skill.get("consumes", [])) + + all_produces.update(produces) + all_consumes.update(consumes) + + for artifact in produces: + consumers = [ + s["name"] for s in skills_metadata + if artifact in s.get("consumes", []) + ] + if consumers: + flows.append({ + "artifact": artifact, + "producer": skill["name"], + "consumers": consumers + }) + + # Find gaps (consumed but not produced) + gaps = all_consumes - all_produces + + return { + "flows": flows, + "gaps": list(gaps), + "fully_covered": len(gaps) == 0 + } + + +def main(): + """CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Recommend skills for a Betty agent" + ) + parser.add_argument( + "agent_purpose", + help="Description of what the agent should do" + ) + parser.add_argument( + "--required-artifacts", + nargs="+", + help="Artifact types the agent needs to work with" + ) + parser.add_argument( + "--output-format", + choices=["yaml", "json", "markdown"], + default="yaml", + help="Output format" + ) + + args = parser.parse_args() + + logger.info(f"Finding skills for agent purpose: {args.agent_purpose}") + + try: + # Get recommendations + result = recommend_skills_for_purpose( + args.agent_purpose, + args.required_artifacts + ) + + # Analyze artifact flow + skills_metadata = list(result["rationale"].values()) + for skill_name, metadata in result["rationale"].items(): + metadata["name"] = skill_name + + flow_analysis = analyze_artifact_flow(skills_metadata) + result["artifact_flow"] = flow_analysis + + # Format output + if args.output_format == "yaml": + print("\n# Recommended Skills for Agent\n") + print(f"# Purpose: {args.agent_purpose}\n") + print("skills_available:") + for skill in result["recommended_skills"]: + print(f" - {skill}") + + print("\n# Rationale:") + for skill_name, rationale in result["rationale"].items(): + print(f"\n# {skill_name}:") + print(f"# {rationale['description']}") + for reason in rationale["reasons"]: + print(f"# - {reason}") + + elif args.output_format == "markdown": + print(f"\n## Recommended Skills for: {args.agent_purpose}\n") + print("### Skills\n") + for skill in result["recommended_skills"]: + rationale = result["rationale"][skill] + print(f"**{skill}**") + print(f"- {rationale['description']}") + for reason in rationale["reasons"]: + print(f" - {reason}") + print() + + else: # json + print(json.dumps(result, indent=2)) + + # Show warnings for gaps + if flow_analysis["gaps"]: + logger.warning(f"\n⚠️ Artifact gaps detected:") + for gap in flow_analysis["gaps"]: + logger.warning(f" - '{gap}' is consumed but not produced") + logger.warning(" Consider adding skills that produce these artifacts") + + logger.info(f"\n✅ Recommended {result['total_recommended']} skills") + + sys.exit(0) + + except Exception as e: + logger.error(f"Failed to compose agent: {e}") + result = { + "ok": False, + "status": "failed", + "error": str(e) + } + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/agent.compose/skill.yaml b/skills/agent.compose/skill.yaml new file mode 100644 index 0000000..15e353e --- /dev/null +++ b/skills/agent.compose/skill.yaml @@ -0,0 +1,102 @@ +name: agent.compose +version: 0.1.0 +description: > + Recommend skills for a Betty agent based on its purpose and responsibilities. + Analyzes artifact flows, ensures skill compatibility, and suggests optimal + skill combinations for agent definitions. + +inputs: + - name: agent_purpose + type: string + required: true + description: Description of what the agent should do (e.g., "Design and validate APIs") + + - name: required_artifacts + type: array + required: false + description: Artifact types the agent needs to work with (e.g., ["openapi-spec"]) + + - name: output_format + type: string + required: false + default: yaml + description: Output format (yaml, json, or markdown) + + - name: include_rationale + type: boolean + required: false + default: true + description: Include explanation of why each skill was recommended + +outputs: + - name: recommended_skills + type: array + description: List of recommended skill names + + - name: skills_with_rationale + type: object + description: Skills with explanation of why they were recommended + + - name: artifact_flow + type: object + description: Diagram showing how artifacts flow between recommended skills + + - name: compatibility_report + type: object + description: Validation that recommended skills work together + +dependencies: + - registry.query + +entrypoints: + - command: /agent/compose + handler: agent_compose.py + runtime: python + description: > + Recommend skills for an agent based on its purpose. Analyzes the registry + to find skills that produce/consume compatible artifacts, ensures no gaps + in artifact flow, and suggests optimal skill combinations. + parameters: + - name: agent_purpose + type: string + required: true + description: What the agent should do + - name: required_artifacts + type: array + required: false + description: Artifact types to work with + - name: output_format + type: string + required: false + default: yaml + description: Output format (yaml, json, markdown) + - name: include_rationale + type: boolean + required: false + default: true + description: Include explanations + permissions: + - filesystem:read + +status: active + +tags: + - agents + - composition + - artifacts + - scaffolding + - interoperability + - layer3 + +# This skill's own artifact metadata +artifact_metadata: + produces: + - type: agent-skill-recommendation + description: Recommended skills list with compatibility analysis for agent definitions + file_pattern: "agent-skills-recommendation.{yaml,json}" + content_type: application/yaml + + consumes: + - type: registry-data + description: Betty Framework registry containing skills and their artifact metadata + required: true diff --git a/skills/agent.define/SKILL.md b/skills/agent.define/SKILL.md new file mode 100644 index 0000000..f0ec0f9 --- /dev/null +++ b/skills/agent.define/SKILL.md @@ -0,0 +1,376 @@ +# agent.define Skill + +Validates and registers agent manifests for the Betty Framework. + +## Purpose + +The `agent.define` skill is the Layer 2 (Reasoning Layer) equivalent of `skill.define`. It validates agent manifests (`agent.yaml`) for schema compliance, verifies skill references, and updates the central Agent Registry. + +## Capabilities + +- Validate agent manifest structure and required fields +- Verify agent name and version formats +- Validate reasoning mode enum values +- Check that all referenced skills exist in skill registry +- Ensure capabilities and skills lists are non-empty +- Validate status lifecycle values +- Register valid agents in `/registry/agents.json` +- Update existing agent entries with new versions + +## Usage + +### Command Line + +```bash +python skills/agent.define/agent_define.py +``` + +### Arguments + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| `manifest_path` | string | Yes | Path to the agent.yaml file to validate | + +### Exit Codes + +- `0`: Validation succeeded and agent was registered +- `1`: Validation failed or registration error + +## Validation Rules + +### Required Fields + +All agent manifests must include: + +| Field | Type | Validation | +|-------|------|------------| +| `name` | string | Must match `^[a-z][a-z0-9._-]*$` | +| `version` | string | Must follow semantic versioning | +| `description` | string | Non-empty string (1-200 chars recommended) | +| `capabilities` | array[string] | Must contain at least one item | +| `skills_available` | array[string] | Must contain at least one item, all skills must exist in registry | +| `reasoning_mode` | enum | Must be `iterative` or `oneshot` | + +### Optional Fields + +| Field | Type | Default | Validation | +|-------|------|---------|------------| +| `status` | enum | `draft` | Must be `draft`, `active`, `deprecated`, or `archived` | +| `context_requirements` | object | `{}` | Any valid object | +| `workflow_pattern` | string | `null` | Any string | +| `example_task` | string | `null` | Any string | +| `error_handling` | object | `{}` | Any valid object | +| `output` | object | `{}` | Any valid object | +| `tags` | array[string] | `[]` | Array of strings | +| `dependencies` | array[string] | `[]` | Array of strings | + +### Name Format + +Agent names must: +- Start with a lowercase letter +- Contain only lowercase letters, numbers, dots, hyphens, underscores +- Follow pattern: `.` + +**Valid**: `api.designer`, `compliance.checker`, `data-migrator` +**Invalid**: `ApiDesigner`, `1agent`, `agent_name` + +### Version Format + +Versions must follow semantic versioning: `MAJOR.MINOR.PATCH[-prerelease]` + +**Valid**: `0.1.0`, `1.0.0`, `2.3.1-beta`, `1.0.0-rc.1` +**Invalid**: `1.0`, `v1.0.0`, `1.0.0.0` + +### Reasoning Mode + +Must be one of: +- `iterative`: Agent can retry with feedback and refine based on errors +- `oneshot`: Agent executes once without retry + +### Skills Validation + +All skills in `skills_available` must exist in the skill registry (`/registry/skills.json`). + +## Response Format + +### Success Response + +```json +{ + "ok": true, + "status": "success", + "errors": [], + "path": "agents/api.designer/agent.yaml", + "details": { + "valid": true, + "errors": [], + "path": "agents/api.designer/agent.yaml", + "manifest": { + "name": "api.designer", + "version": "0.1.0", + "description": "Design RESTful APIs...", + "capabilities": [...], + "skills_available": [...], + "reasoning_mode": "iterative" + }, + "status": "registered", + "registry_updated": true + } +} +``` + +### Failure Response + +```json +{ + "ok": false, + "status": "failed", + "errors": [ + "Missing required fields: capabilities, skills_available", + "Invalid reasoning_mode: 'hybrid'. Must be one of: iterative, oneshot" + ], + "path": "agents/bad.agent/agent.yaml", + "details": { + "valid": false, + "errors": [ + "Missing required fields: capabilities, skills_available", + "Invalid reasoning_mode: 'hybrid'. Must be one of: iterative, oneshot" + ], + "path": "agents/bad.agent/agent.yaml" + } +} +``` + +## Examples + +### Example 1: Validate Iterative Agent + +**Agent Manifest** (`agents/api.designer/agent.yaml`): +```yaml +name: api.designer +version: 0.1.0 +description: "Design RESTful APIs following enterprise guidelines" + +capabilities: + - Design RESTful APIs from requirements + - Apply Zalando guidelines automatically + - Generate OpenAPI 3.1 specs + - Iteratively refine based on validation feedback + +skills_available: + - api.define + - api.validate + - api.generate-models + +reasoning_mode: iterative + +status: draft + +tags: + - api + - design + - openapi +``` + +**Command**: +```bash +python skills/agent.define/agent_define.py agents/api.designer/agent.yaml +``` + +**Output**: +```json +{ + "ok": true, + "status": "success", + "errors": [], + "path": "agents/api.designer/agent.yaml", + "details": { + "valid": true, + "status": "registered", + "registry_updated": true + } +} +``` + +### Example 2: Validation Errors + +**Agent Manifest** (`agents/bad.agent/agent.yaml`): +```yaml +name: BadAgent # Invalid: must be lowercase +version: 1.0 # Invalid: must be semver +description: "Test agent" +capabilities: [] # Invalid: must have at least one +skills_available: + - nonexistent.skill # Invalid: skill doesn't exist +reasoning_mode: hybrid # Invalid: must be iterative or oneshot +``` + +**Command**: +```bash +python skills/agent.define/agent_define.py agents/bad.agent/agent.yaml +``` + +**Output**: +```json +{ + "ok": false, + "status": "failed", + "errors": [ + "Invalid name: Invalid agent name: 'BadAgent'. Must start with lowercase letter...", + "Invalid version: Invalid version: '1.0'. Must follow semantic versioning...", + "Invalid reasoning_mode: Invalid reasoning_mode: 'hybrid'. Must be one of: iterative, oneshot", + "capabilities must contain at least one item", + "Skills not found in registry: nonexistent.skill" + ], + "path": "agents/bad.agent/agent.yaml" +} +``` + +### Example 3: Oneshot Agent + +**Agent Manifest** (`agents/api.analyzer/agent.yaml`): +```yaml +name: api.analyzer +version: 0.1.0 +description: "Analyze API specifications for compatibility" + +capabilities: + - Detect breaking changes between API versions + - Generate compatibility reports + - Suggest migration paths + +skills_available: + - api.compatibility + +reasoning_mode: oneshot + +output: + success: + - Compatibility report + - Breaking changes list + failure: + - Error analysis + +status: active + +tags: + - api + - analysis + - compatibility +``` + +**Command**: +```bash +python skills/agent.define/agent_define.py agents/api.analyzer/agent.yaml +``` + +**Result**: Agent validated and registered successfully. + +## Integration + +### With Registry + +The skill automatically updates `/registry/agents.json`: + +```json +{ + "registry_version": "1.0.0", + "generated_at": "2025-10-23T10:30:00Z", + "agents": [ + { + "name": "api.designer", + "version": "0.1.0", + "description": "Design RESTful APIs following enterprise guidelines", + "reasoning_mode": "iterative", + "skills_available": ["api.define", "api.validate", "api.generate-models"], + "capabilities": ["Design RESTful APIs from requirements", ...], + "status": "draft", + "tags": ["api", "design", "openapi"], + "dependencies": [] + } + ] +} +``` + +### With Other Skills + +- **Depends on**: `skill.define` (for skill registry validation) +- **Used by**: Future `command.define` skill (to register commands that invoke agents) +- **Complements**: `workflow.compose` (agents orchestrate skills; workflows execute fixed sequences) + +## Common Errors + +### Missing Skills in Registry + +**Error**: +``` +Skills not found in registry: api.nonexistent, data.missing +``` + +**Solution**: Ensure all skills in `skills_available` are registered in `/registry/skills.json`. Check skill names for typos. + +### Invalid Reasoning Mode + +**Error**: +``` +Invalid reasoning_mode: 'hybrid'. Must be one of: iterative, oneshot +``` + +**Solution**: Use `iterative` for agents that retry with feedback, or `oneshot` for deterministic execution. + +### Empty Capabilities + +**Error**: +``` +capabilities must contain at least one item +``` + +**Solution**: Add at least one capability string describing what the agent can do. + +### Invalid Name Format + +**Error**: +``` +Invalid agent name: 'API-Designer'. Must start with lowercase letter... +``` + +**Solution**: Use lowercase names following pattern `.` (e.g., `api.designer`). + +## Development + +### Testing + +Create test agent manifests in `/agents/test/`: + +```bash +# Create test directory +mkdir -p agents/test.agent + +# Create minimal test manifest +cat > agents/test.agent/agent.yaml << EOF +name: test.agent +version: 0.1.0 +description: "Test agent" +capabilities: + - Test capability +skills_available: + - skill.define +reasoning_mode: oneshot +status: draft +EOF + +# Validate +python skills/agent.define/agent_define.py agents/test.agent/agent.yaml +``` + +### Registry Location + +- Skill registry: `/registry/skills.json` (read for validation) +- Agent registry: `/registry/agents.json` (updated by this skill) + +## See Also + +- [Agent Schema Reference](../../docs/agent-schema-reference.md) - Complete field specifications +- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer architecture overview +- [Agent Implementation Plan](../../docs/agent-define-implementation-plan.md) - Implementation details +- `/agents/README.md` - Agent directory documentation diff --git a/skills/agent.define/__init__.py b/skills/agent.define/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/agent.define/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/agent.define/agent_define.py b/skills/agent.define/agent_define.py new file mode 100755 index 0000000..ee629a4 --- /dev/null +++ b/skills/agent.define/agent_define.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +""" +agent_define.py – Implementation of the agent.define Skill +Validates agent manifests (agent.yaml) and registers them in the Agent 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_AGENT_FIELDS, + AGENTS_REGISTRY_FILE, + REGISTRY_FILE, +) +from betty.enums import AgentStatus, ReasoningMode +from betty.validation import ( + validate_path, + validate_manifest_fields, + validate_agent_name, + validate_version, + validate_reasoning_mode, + validate_skills_exist +) +from betty.logging_utils import setup_logger +from betty.errors import AgentValidationError, AgentRegistryError, format_error_response +from betty.models import AgentManifest +from betty.file_utils import atomic_write_json + +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]: + """ + Build standardized response dictionary. + + Args: + ok: Whether operation succeeded + path: Path to agent 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_agent_manifest(path: str) -> Dict[str, Any]: + """ + Load and parse an agent manifest from YAML file. + + Args: + path: Path to agent manifest file + + Returns: + Parsed manifest dictionary + + Raises: + AgentValidationError: If manifest cannot be loaded or parsed + """ + try: + with open(path) as f: + manifest = yaml.safe_load(f) + return manifest + except FileNotFoundError: + raise AgentValidationError(f"Manifest file not found: {path}") + except yaml.YAMLError as e: + raise AgentValidationError(f"Failed to parse YAML: {e}") + + +def load_skill_registry() -> Dict[str, Any]: + """ + Load skill registry for validation. + + Returns: + Skill registry dictionary + + Raises: + AgentValidationError: If registry cannot be loaded + """ + try: + with open(REGISTRY_FILE) as f: + return json.load(f) + except FileNotFoundError: + raise AgentValidationError(f"Skill registry not found: {REGISTRY_FILE}") + except json.JSONDecodeError as e: + raise AgentValidationError(f"Failed to parse skill registry: {e}") + + +def validate_agent_schema(manifest: Dict[str, Any]) -> List[str]: + """ + Validate agent manifest using Pydantic schema. + + Args: + manifest: Agent manifest dictionary + + Returns: + List of validation errors (empty if valid) + """ + errors: List[str] = [] + + try: + AgentManifest.model_validate(manifest) + logger.info("Pydantic schema validation passed for agent manifest") + except PydanticValidationError as exc: + logger.warning("Pydantic schema validation failed for agent 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 an agent manifest meets all requirements. + + Validation checks: + 1. Required fields are present + 2. Name format is valid + 3. Version format is valid + 4. Reasoning mode is valid + 5. All referenced skills exist in skill registry + 6. Capabilities list is non-empty + 7. Skills list is non-empty + + Args: + path: Path to agent 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 agent manifest: {path}") + + errors = [] + + # Load manifest + try: + manifest = load_agent_manifest(path) + except AgentValidationError as e: + return { + "valid": False, + "errors": [str(e)], + "path": path + } + + # Check required fields first so high-level issues are reported clearly + missing = validate_manifest_fields(manifest, REQUIRED_AGENT_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 while continuing custom validation + schema_errors = validate_agent_schema(manifest) + errors.extend(schema_errors) + + name = manifest.get("name") + if name is not None: + try: + validate_agent_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}") + + reasoning_mode = manifest.get("reasoning_mode") + if reasoning_mode is not None: + try: + validate_reasoning_mode(reasoning_mode) + except Exception as e: + errors.append(f"Invalid reasoning_mode: {str(e)}") + logger.warning(f"Invalid reasoning_mode: {e}") + elif "reasoning_mode" not in missing: + errors.append("reasoning_mode must be provided") + logger.warning("Reasoning mode missing") + + # Validate capabilities is non-empty + capabilities = manifest.get("capabilities", []) + if not capabilities or len(capabilities) == 0: + errors.append("capabilities must contain at least one item") + logger.warning("Empty capabilities list") + + # Validate skills_available is non-empty + skills_available = manifest.get("skills_available", []) + if not skills_available or len(skills_available) == 0: + errors.append("skills_available must contain at least one item") + logger.warning("Empty skills_available list") + + # Validate all skills exist in skill registry + if skills_available: + try: + skill_registry = load_skill_registry() + missing_skills = validate_skills_exist(skills_available, skill_registry) + if missing_skills: + errors.append(f"Skills not found in registry: {', '.join(missing_skills)}") + logger.warning(f"Missing skills: {missing_skills}") + except AgentValidationError as e: + errors.append(f"Could not validate skills: {str(e)}") + logger.error(f"Skill validation error: {e}") + + # Validate status if present + if "status" in manifest: + valid_statuses = [s.value for s in AgentStatus] + 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']}") + + if errors: + logger.warning(f"Validation failed with {len(errors)} error(s)") + return { + "valid": False, + "errors": errors, + "path": path + } + + logger.info("✅ Agent manifest validation passed") + return { + "valid": True, + "errors": [], + "path": path, + "manifest": manifest + } + + +def load_agent_registry() -> Dict[str, Any]: + """ + Load existing agent registry. + + Returns: + Agent registry dictionary, or new empty registry if file doesn't exist + """ + if not os.path.exists(AGENTS_REGISTRY_FILE): + logger.info("Agent registry not found, creating new registry") + return { + "registry_version": "1.0.0", + "generated_at": datetime.now(timezone.utc).isoformat(), + "agents": [] + } + + try: + with open(AGENTS_REGISTRY_FILE) as f: + registry = json.load(f) + logger.info(f"Loaded agent registry with {len(registry.get('agents', []))} agent(s)") + return registry + except json.JSONDecodeError as e: + raise AgentRegistryError(f"Failed to parse agent registry: {e}") + + +def update_agent_registry(manifest: Dict[str, Any]) -> bool: + """ + Add or update agent in the agent registry. + + Args: + manifest: Validated agent manifest + + Returns: + True if registry was updated successfully + + Raises: + AgentRegistryError: If registry update fails + """ + logger.info(f"Updating agent registry for: {manifest['name']}") + + # Load existing registry + registry = load_agent_registry() + + # Create registry entry + entry = { + "name": manifest["name"], + "version": manifest["version"], + "description": manifest["description"], + "reasoning_mode": manifest["reasoning_mode"], + "skills_available": manifest["skills_available"], + "capabilities": manifest.get("capabilities", []), + "status": manifest.get("status", "draft"), + "tags": manifest.get("tags", []), + "dependencies": manifest.get("dependencies", []) + } + + # Check if agent already exists + agents = registry.get("agents", []) + existing_index = None + for i, agent in enumerate(agents): + if agent["name"] == manifest["name"]: + existing_index = i + break + + if existing_index is not None: + # Update existing agent + agents[existing_index] = entry + logger.info(f"Updated existing agent: {manifest['name']}") + else: + # Add new agent + agents.append(entry) + logger.info(f"Added new agent: {manifest['name']}") + + registry["agents"] = agents + registry["generated_at"] = datetime.now(timezone.utc).isoformat() + + # Write registry back to disk atomically + try: + atomic_write_json(AGENTS_REGISTRY_FILE, registry) + logger.info(f"Agent registry updated successfully") + return True + except Exception as e: + raise AgentRegistryError(f"Failed to write agent registry: {e}") + + +def main(): + """Main CLI entry point.""" + if len(sys.argv) < 2: + message = "Usage: agent_define.py " + 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_agent_registry(validation["manifest"]) + details["status"] = "registered" + details["registry_updated"] = registry_updated + except AgentRegistryError 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": "Agent 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 AgentValidationError 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() diff --git a/skills/agent.define/skill.yaml b/skills/agent.define/skill.yaml new file mode 100644 index 0000000..563ad79 --- /dev/null +++ b/skills/agent.define/skill.yaml @@ -0,0 +1,45 @@ +name: agent.define +version: 0.1.0 +description: > + Validates and registers agent manifests for the Betty Framework. + Ensures schema compliance, validates skill references, and updates the Agent Registry. + +inputs: + - name: manifest_path + type: string + required: true + description: Path to the agent.yaml file to validate + +outputs: + - name: validation_result + type: object + description: Validation results including errors and warnings + - name: registry_updated + type: boolean + description: Whether agent was successfully registered + +dependencies: + - skill.define + +status: active + +entrypoints: + - command: /agent/define + handler: agent_define.py + runtime: python + description: > + Validate an agent manifest and register it in the Agent Registry. + parameters: + - name: manifest_path + type: string + required: true + description: Path to the agent.yaml file to validate + permissions: + - filesystem:read + - filesystem:write + +tags: + - agents + - validation + - registry + - layer2 diff --git a/skills/agent.run/SKILL.md b/skills/agent.run/SKILL.md new file mode 100644 index 0000000..01e6777 --- /dev/null +++ b/skills/agent.run/SKILL.md @@ -0,0 +1,585 @@ +# agent.run + +**Version:** 0.1.0 +**Status:** Active +**Tags:** agents, execution, claude-api, orchestration, layer2 + +## Overview + +The `agent.run` skill executes registered Betty agents by orchestrating the complete agent lifecycle: loading manifests, generating Claude-friendly prompts, invoking the Claude API (or simulating), executing planned skills, and logging all results. + +This skill is the primary execution engine for Betty agents, enabling them to operate in both **iterative** and **oneshot** reasoning modes. It handles the translation between agent manifests and Claude API calls, manages skill invocation, and provides comprehensive logging for auditability. + +## Features + +- ✅ Load agent manifests from path or agent name +- ✅ Generate Claude-optimized system prompts with capabilities and workflow patterns +- ✅ Optional Claude API integration (with mock fallback for development) +- ✅ Support for both iterative and oneshot reasoning modes +- ✅ Skill selection and execution orchestration +- ✅ Comprehensive execution logging to `agent_logs/_.json` +- ✅ Structured JSON output for programmatic integration +- ✅ Error handling with detailed diagnostics +- ✅ Validation of agent manifests and available skills + +## Usage + +### Command Line + +```bash +# Execute agent by name +python skills/agent.run/agent_run.py api.designer + +# Execute with task context +python skills/agent.run/agent_run.py api.designer "Design a REST API for user management" + +# Execute from manifest path +python skills/agent.run/agent_run.py agents/api.designer/agent.yaml "Create authentication API" + +# Execute without saving logs +python skills/agent.run/agent_run.py api.designer "Design API" --no-save-log +``` + +### As a Skill (Programmatic) + +```python +import sys +import os +sys.path.insert(0, os.path.abspath("./")) + +from skills.agent.run.agent_run import run_agent + +# Execute agent +result = run_agent( + agent_path="api.designer", + task_context="Design a REST API for user management with authentication", + save_log=True +) + +if result["ok"]: + print(f"Agent executed successfully!") + print(f"Skills invoked: {result['details']['summary']['skills_executed']}") + print(f"Log saved to: {result['details']['log_path']}") +else: + print(f"Execution failed: {result['errors']}") +``` + +### Via Claude Code Plugin + +```bash +# Using the Betty plugin command +/agent/run api.designer "Design authentication API" + +# With full path +/agent/run agents/api.designer/agent.yaml "Create user management endpoints" +``` + +## Input Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `agent_path` | string | Yes | - | Path to agent.yaml or agent name (e.g., `api.designer`) | +| `task_context` | string | No | None | Task or query to provide to the agent | +| `save_log` | boolean | No | true | Whether to save execution log to disk | + +## Output Schema + +```json +{ + "ok": true, + "status": "success", + "timestamp": "2025-10-23T14:30:00Z", + "errors": [], + "details": { + "timestamp": "2025-10-23T14:30:00Z", + "agent": { + "name": "api.designer", + "version": "0.1.0", + "description": "Design RESTful APIs...", + "reasoning_mode": "iterative", + "status": "active" + }, + "task_context": "Design a REST API for user management", + "prompt": "You are api.designer, a specialized Betty Framework agent...", + "skills_available": [ + { + "name": "api.define", + "description": "Create OpenAPI specifications", + "status": "active" + } + ], + "missing_skills": [], + "claude_response": { + "analysis": "I will design a comprehensive user management API...", + "skills_to_invoke": [ + { + "skill": "api.define", + "purpose": "Create initial OpenAPI spec", + "inputs": {"guidelines": "zalando"}, + "order": 1 + } + ], + "reasoning": "Following API design workflow pattern" + }, + "execution_results": [ + { + "skill": "api.define", + "purpose": "Create initial OpenAPI spec", + "status": "simulated", + "timestamp": "2025-10-23T14:30:05Z", + "output": { + "success": true, + "note": "Simulated execution of api.define" + } + } + ], + "summary": { + "skills_planned": 3, + "skills_executed": 3, + "success": true + }, + "log_path": "/home/user/betty/agent_logs/api.designer_20251023_143000.json" + } +} +``` + +## Reasoning Modes + +### Oneshot Mode + +In **oneshot** mode, the agent analyzes the complete task and plans all skill invocations upfront in a single pass. The execution follows the predetermined plan without dynamic adjustment. + +**Best for:** +- Well-defined tasks with predictable workflows +- Tasks where all steps can be determined in advance +- Performance-critical scenarios requiring minimal API calls + +**Example Agent:** +```yaml +name: api.generator +reasoning_mode: oneshot +workflow_pattern: | + 1. Define API structure + 2. Validate specification + 3. Generate models +``` + +### Iterative Mode + +In **iterative** mode, the agent analyzes results after each skill invocation and dynamically determines the next steps. It can retry failed operations, adjust its approach based on feedback, or invoke additional skills as needed. + +**Best for:** +- Complex tasks requiring adaptive decision-making +- Tasks with validation/refinement loops +- Scenarios where results influence subsequent steps + +**Example Agent:** +```yaml +name: api.designer +reasoning_mode: iterative +workflow_pattern: | + 1. Analyze requirements + 2. Draft OpenAPI spec + 3. Validate (if fails, refine and retry) + 4. Generate models +``` + +## Examples + +### Example 1: Execute API Designer + +```bash +python skills/agent.run/agent_run.py api.designer \ + "Create a REST API for managing blog posts with CRUD operations" +``` + +**Output:** +``` +================================================================================ +AGENT EXECUTION: api.designer +================================================================================ + +Agent: api.designer v0.1.0 +Mode: iterative +Status: active + +Task: Create a REST API for managing blog posts with CRUD operations + +-------------------------------------------------------------------------------- +CLAUDE RESPONSE: +-------------------------------------------------------------------------------- +{ + "analysis": "I will design a RESTful API following best practices...", + "skills_to_invoke": [ + { + "skill": "api.define", + "purpose": "Create initial OpenAPI specification", + "inputs": {"guidelines": "zalando", "format": "openapi-3.1"}, + "order": 1 + }, + { + "skill": "api.validate", + "purpose": "Validate the specification for compliance", + "inputs": {"strict_mode": true}, + "order": 2 + } + ] +} + +-------------------------------------------------------------------------------- +EXECUTION RESULTS: +-------------------------------------------------------------------------------- + + ✓ api.define + Purpose: Create initial OpenAPI specification + Status: simulated + + ✓ api.validate + Purpose: Validate the specification for compliance + Status: simulated + +📝 Log saved to: /home/user/betty/agent_logs/api.designer_20251023_143000.json + +================================================================================ +EXECUTION COMPLETE +================================================================================ +``` + +### Example 2: Execute with Direct Path + +```bash +python skills/agent.run/agent_run.py \ + agents/api.analyzer/agent.yaml \ + "Analyze this OpenAPI spec for compatibility issues" +``` + +### Example 3: Execute Without Logging + +```bash +python skills/agent.run/agent_run.py api.designer \ + "Design authentication API" \ + --no-save-log +``` + +### Example 4: Programmatic Integration + +```python +from skills.agent.run.agent_run import run_agent, load_agent_manifest + +# Load and inspect agent before running +manifest = load_agent_manifest("api.designer") +print(f"Agent capabilities: {manifest['capabilities']}") + +# Execute with custom context +result = run_agent( + agent_path="api.designer", + task_context="Design GraphQL API for e-commerce", + save_log=True +) + +if result["ok"]: + # Access execution details + claude_response = result["details"]["claude_response"] + execution_results = result["details"]["execution_results"] + + print(f"Claude planned {len(claude_response['skills_to_invoke'])} skills") + print(f"Executed {len(execution_results)} skills") + + # Check individual skill results + for exec_result in execution_results: + print(f" - {exec_result['skill']}: {exec_result['status']}") +``` + +## Agent Manifest Requirements + +For `agent.run` to successfully execute an agent, the agent manifest must include: + +### Required Fields + +```yaml +name: agent.name # Must match pattern ^[a-z][a-z0-9._-]*$ +version: 0.1.0 # Semantic version +description: "..." # Clear description +capabilities: # List of capabilities + - "Capability 1" + - "Capability 2" +skills_available: # List of Betty skills + - skill.name.1 + - skill.name.2 +reasoning_mode: iterative # 'iterative' or 'oneshot' +``` + +### Recommended Fields + +```yaml +workflow_pattern: | # Recommended workflow steps + 1. Step 1 + 2. Step 2 + 3. Step 3 + +context_requirements: # Optional context hints + guidelines: string + domain: string + +error_handling: # Error handling config + max_retries: 3 + timeout_seconds: 300 + +status: active # Agent status (draft/active/deprecated) +tags: # Categorization tags + - tag1 + - tag2 +``` + +## Claude API Integration + +The skill supports both real Claude API calls and mock simulation: + +### Real API Mode (Production) + +Set the `ANTHROPIC_API_KEY` environment variable: + +```bash +export ANTHROPIC_API_KEY="sk-ant-..." +python skills/agent.run/agent_run.py api.designer "Design API" +``` + +The skill will: +1. Detect the API key +2. Use the Anthropic Python SDK +3. Call Claude 3.5 Sonnet with the constructed prompt +4. Parse the structured JSON response +5. Execute the skills based on Claude's plan + +### Mock Mode (Development) + +Without an API key, the skill generates intelligent mock responses: + +```bash +python skills/agent.run/agent_run.py api.designer "Design API" +``` + +The skill will: +1. Detect no API key +2. Generate plausible skill selections based on agent type +3. Simulate Claude's reasoning +4. Execute skills with simulated outputs + +## Execution Logging + +All agent executions are logged to `agent_logs/_.json` with: + +- **Timestamp**: ISO 8601 UTC timestamp +- **Agent Info**: Name, version, description, mode, status +- **Task Context**: User-provided task or query +- **Prompt**: Complete Claude system prompt +- **Skills Available**: Registered skills with metadata +- **Missing Skills**: Skills referenced but not found +- **Claude Response**: Full API response or mock +- **Execution Results**: Output from each skill invocation +- **Summary**: Counts, success status, timing + +### Log File Structure + +```json +{ + "timestamp": "2025-10-23T14:30:00Z", + "agent": { /* agent metadata */ }, + "task_context": "Design API for...", + "prompt": "You are api.designer...", + "skills_available": [ /* skill info */ ], + "missing_skills": [], + "claude_response": { /* Claude's plan */ }, + "execution_results": [ /* skill outputs */ ], + "summary": { + "skills_planned": 3, + "skills_executed": 3, + "success": true + } +} +``` + +### Accessing Logs + +```bash +# View latest log for an agent +cat agent_logs/api.designer_latest.json | jq '.' + +# View specific execution +cat agent_logs/api.designer_20251023_143000.json | jq '.summary' + +# List all logs for an agent +ls -lt agent_logs/api.designer_*.json +``` + +## Error Handling + +### Common Errors + +**Agent Not Found** +```json +{ + "ok": false, + "status": "failed", + "errors": ["Agent not found: my.agent"], + "details": { + "error": { + "type": "BettyError", + "message": "Agent not found: my.agent", + "details": { + "agent_path": "my.agent", + "expected_path": "/home/user/betty/agents/my.agent/agent.yaml", + "suggestion": "Use 'betty agent list' to see available agents" + } + } + } +} +``` + +**Invalid Agent Manifest** +```json +{ + "ok": false, + "errors": ["Agent manifest missing required fields: reasoning_mode, capabilities"], + "details": { + "error": { + "type": "BettyError", + "details": { + "missing_fields": ["reasoning_mode", "capabilities"] + } + } + } +} +``` + +**Skill Not Found** +- The execution continues but logs missing skills in `missing_skills` array +- Warning logged for each missing skill +- Agent may not function as intended if critical skills are missing + +### Debugging Tips + +1. **Check agent manifest**: Validate with `betty agent validate ` +2. **Verify skills**: Ensure all `skills_available` are registered +3. **Review logs**: Check `agent_logs/_latest.json` for details +4. **Enable debug logging**: Set `BETTY_LOG_LEVEL=DEBUG` +5. **Test with mock mode**: Remove API key to test workflow logic + +## Best Practices + +### 1. Agent Design + +- Define clear, specific capabilities in agent manifests +- Choose appropriate reasoning mode for the task complexity +- Provide detailed workflow patterns to guide Claude +- Include context requirements for optimal prompts + +### 2. Task Context + +- Provide specific, actionable task descriptions +- Include relevant domain context when needed +- Reference specific requirements or constraints +- Use examples to clarify ambiguous requests + +### 3. Logging + +- Keep logs enabled for production (default: `save_log=true`) +- Review logs regularly for debugging and auditing +- Archive old logs periodically to manage disk space +- Use log summaries to track agent performance + +### 4. Error Recovery + +- In iterative mode, agents can retry failed skills +- Review error details in logs for root cause analysis +- Validate agent manifests before deployment +- Test with mock mode before using real API calls + +### 5. Performance + +- Use oneshot mode for predictable, fast execution +- Cache agent manifests when running repeatedly +- Monitor Claude API usage and costs +- Consider skill execution time when designing workflows + +## Integration with Betty Framework + +### Skill Dependencies + +`agent.run` depends on: +- **agent.define**: For creating agent manifests +- **Skill registry**: For validating available skills +- **Betty configuration**: For paths and settings + +### Plugin Integration + +The skill is registered in `plugin.yaml` as: +```yaml +- name: agent/run + description: Execute a registered Betty agent + handler: + runtime: python + script: skills/agent.run/agent_run.py + parameters: + - name: agent_path + type: string + required: true +``` + +This enables Claude Code to invoke agents directly: +``` +User: "Run the API designer agent to create a user management API" +Claude: [Invokes /agent/run api.designer "create user management API"] +``` + +## Related Skills + +- **agent.define** - Create and register new agent manifests +- **agent.validate** - Validate agent manifests before execution +- **run.agent** - Legacy simulation tool (read-only, no execution) +- **skill.define** - Register skills that agents can invoke +- **hook.simulate** - Test hooks before registration + +## Changelog + +### v0.1.0 (2025-10-23) +- Initial implementation +- Support for iterative and oneshot reasoning modes +- Claude API integration with mock fallback +- Execution logging to agent_logs/ +- Comprehensive error handling +- CLI and programmatic interfaces +- Plugin integration for Claude Code + +## Future Enhancements + +Planned features for future versions: + +- **v0.2.0**: + - Real Claude API integration (currently mocked) + - Skill execution (currently simulated) + - Iterative feedback loops + - Performance metrics + +- **v0.3.0**: + - Agent context persistence + - Multi-agent orchestration + - Streaming responses + - Parallel skill execution + +- **v0.4.0**: + - Agent memory and learning + - Custom LLM backends + - Agent marketplace integration + - A/B testing framework + +## License + +Part of the Betty Framework. See project LICENSE for details. + +## Support + +For issues, questions, or contributions: +- GitHub: [Betty Framework Repository] +- Documentation: `/docs/skills/agent.run.md` +- Examples: `/examples/agents/` diff --git a/skills/agent.run/__init__.py b/skills/agent.run/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/agent.run/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/agent.run/agent_run.py b/skills/agent.run/agent_run.py new file mode 100644 index 0000000..ec93c80 --- /dev/null +++ b/skills/agent.run/agent_run.py @@ -0,0 +1,756 @@ +#!/usr/bin/env python3 +""" +agent_run.py – Implementation of the agent.run Skill + +Executes a registered Betty agent by loading its manifest, constructing a Claude-friendly +prompt, invoking the Claude API (or simulating), and logging execution results. + +This skill supports both iterative and oneshot reasoning modes and can execute +skills based on the agent's workflow pattern. +""" +import os +import sys +import yaml +import json +from typing import Dict, Any, List, Optional +from datetime import datetime, timezone +from pathlib import Path + + +from betty.config import ( + AGENTS_DIR, AGENTS_REGISTRY_FILE, REGISTRY_FILE, + get_agent_manifest_path, get_skill_manifest_path, + BETTY_HOME +) +from betty.validation import validate_path +from betty.logging_utils import setup_logger +from betty.errors import BettyError, format_error_response +from betty.telemetry_capture import capture_skill_execution, capture_audit_entry +from utils.telemetry_utils import capture_telemetry + +logger = setup_logger(__name__) + +# Agent logs directory +AGENT_LOGS_DIR = os.path.join(BETTY_HOME, "agent_logs") + + +def build_response( + ok: bool, + errors: Optional[List[str]] = None, + details: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: + """ + Build standardized response. + + Args: + ok: Whether the operation was successful + errors: List of error messages + details: Additional details to include + + Returns: + Standardized response dictionary + """ + response: Dict[str, Any] = { + "ok": ok, + "status": "success" if ok else "failed", + "errors": errors or [], + "timestamp": datetime.now(timezone.utc).isoformat() + } + if details is not None: + response["details"] = details + return response + + +def load_agent_manifest(agent_path: str) -> Dict[str, Any]: + """ + Load agent manifest from path or agent name. + + Args: + agent_path: Path to agent.yaml or agent name (e.g., api.designer) + + Returns: + Agent manifest dictionary + + Raises: + BettyError: If agent cannot be loaded or is invalid + """ + # Check if it's a direct path to agent.yaml + if os.path.exists(agent_path) and agent_path.endswith('.yaml'): + manifest_path = agent_path + # Check if it's an agent name + else: + manifest_path = get_agent_manifest_path(agent_path) + if not os.path.exists(manifest_path): + raise BettyError( + f"Agent not found: {agent_path}", + details={ + "agent_path": agent_path, + "expected_path": manifest_path, + "suggestion": "Use 'betty agent list' to see available agents" + } + ) + + try: + with open(manifest_path) as f: + manifest = yaml.safe_load(f) + + if not isinstance(manifest, dict): + raise BettyError("Agent manifest must be a dictionary") + + # Validate required fields + required_fields = ["name", "version", "description", "capabilities", + "skills_available", "reasoning_mode"] + missing = [f for f in required_fields if f not in manifest] + if missing: + raise BettyError( + f"Agent manifest missing required fields: {', '.join(missing)}", + details={"missing_fields": missing} + ) + + return manifest + except yaml.YAMLError as e: + raise BettyError(f"Invalid YAML in agent manifest: {e}") + + +def load_skill_registry() -> Dict[str, Any]: + """ + Load the skills registry. + + Returns: + Skills registry dictionary + """ + try: + with open(REGISTRY_FILE) as f: + return json.load(f) + except FileNotFoundError: + logger.warning(f"Skills registry not found: {REGISTRY_FILE}") + return {"skills": []} + except json.JSONDecodeError as e: + raise BettyError(f"Invalid JSON in skills registry: {e}") + + +def get_skill_info(skill_name: str, registry: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """ + Get skill information from registry. + + Args: + skill_name: Name of the skill + registry: Skills registry + + Returns: + Skill info dictionary or None if not found + """ + for skill in registry.get("skills", []): + if skill.get("name") == skill_name: + return skill + return None + + +def construct_agent_prompt( + agent_manifest: Dict[str, Any], + task_context: Optional[str] = None +) -> str: + """ + Construct a Claude-friendly prompt for the agent. + + Args: + agent_manifest: Agent manifest dictionary + task_context: User-provided task or query + + Returns: + Constructed system prompt string suitable for Claude API + """ + agent_name = agent_manifest.get("name", "unknown") + description = agent_manifest.get("description", "") + capabilities = agent_manifest.get("capabilities", []) + skills_available = agent_manifest.get("skills_available", []) + reasoning_mode = agent_manifest.get("reasoning_mode", "oneshot") + workflow_pattern = agent_manifest.get("workflow_pattern", "") + context_requirements = agent_manifest.get("context_requirements", {}) + + # Build system prompt + prompt = f"""You are {agent_name}, a specialized Betty Framework agent. + +## AGENT DESCRIPTION +{description} + +## CAPABILITIES +You have the following capabilities: +""" + for cap in capabilities: + prompt += f" • {cap}\n" + + prompt += f""" +## REASONING MODE +{reasoning_mode.upper()}: """ + + if reasoning_mode == "iterative": + prompt += """You will analyze results from each skill invocation and determine +the next steps dynamically. You may retry failed operations or adjust your +approach based on feedback.""" + else: + prompt += """You will plan and execute all necessary skills in a single pass. +Analyze the task completely before determining the sequence of skill invocations.""" + + prompt += """ + +## AVAILABLE SKILLS +You have access to the following Betty skills: +""" + for skill in skills_available: + prompt += f" • {skill}\n" + + if workflow_pattern: + prompt += f""" +## RECOMMENDED WORKFLOW +{workflow_pattern} +""" + + if context_requirements: + prompt += """ +## CONTEXT REQUIREMENTS +The following context may be required for optimal performance: +""" + for key, value_type in context_requirements.items(): + prompt += f" • {key}: {value_type}\n" + + if task_context: + prompt += f""" +## TASK +{task_context} + +## INSTRUCTIONS +Analyze the task above and respond with a JSON object describing your execution plan: + +{{ + "analysis": "Brief analysis of the task", + "skills_to_invoke": [ + {{ + "skill": "skill.name", + "purpose": "Why this skill is needed", + "inputs": {{"param": "value"}}, + "order": 1 + }} + ], + "reasoning": "Explanation of your approach" +}} + +Select skills from your available skills list and arrange them according to the +workflow pattern. Ensure the sequence makes logical sense for accomplishing the task. +""" + else: + prompt += """ +## READY STATE +You are initialized and ready to accept tasks. When given a task, you will: +1. Analyze the requirements +2. Select appropriate skills from your available skills +3. Determine the execution order based on your workflow pattern +4. Provide a structured execution plan +""" + + return prompt + + +def call_claude_api(prompt: str, agent_name: str) -> Dict[str, Any]: + """ + Call the Claude API with the constructed prompt. + + Currently simulates the API call. In production, this would: + 1. Use the Anthropic API client + 2. Send the prompt with appropriate parameters + 3. Parse the structured response + + Args: + prompt: The constructed system prompt + agent_name: Name of the agent (for context) + + Returns: + Claude's response (currently mocked) + """ + # Check if we have ANTHROPIC_API_KEY in environment + api_key = os.environ.get("ANTHROPIC_API_KEY") + + if api_key: + logger.info("Anthropic API key found - would call real API") + # TODO: Implement actual API call + # from anthropic import Anthropic + # client = Anthropic(api_key=api_key) + # response = client.messages.create( + # model="claude-3-5-sonnet-20241022", + # max_tokens=4096, + # system=prompt, + # messages=[{"role": "user", "content": "Execute the task"}] + # ) + # return parse_claude_response(response) + + logger.info("No API key found - using mock response") + return generate_mock_response(prompt, agent_name) + + +def generate_mock_response(prompt: str, agent_name: str) -> Dict[str, Any]: + """ + Generate a mock Claude response for simulation. + + Args: + prompt: The system prompt + agent_name: Name of the agent + + Returns: + Mock response dictionary + """ + # Extract task from prompt if present + task_section = "" + if "## TASK" in prompt: + task_start = prompt.index("## TASK") + task_end = prompt.index("## INSTRUCTIONS") if "## INSTRUCTIONS" in prompt else len(prompt) + task_section = prompt[task_start:task_end].replace("## TASK", "").strip() + + # Generate plausible skill selections based on agent name + skills_to_invoke = [] + + if "api.designer" in agent_name: + skills_to_invoke = [ + { + "skill": "api.define", + "purpose": "Create initial OpenAPI specification from requirements", + "inputs": {"guidelines": "zalando", "format": "openapi-3.1"}, + "order": 1 + }, + { + "skill": "api.validate", + "purpose": "Validate the generated specification for compliance", + "inputs": {"strict_mode": True}, + "order": 2 + }, + { + "skill": "api.generate-models", + "purpose": "Generate type-safe models from validated spec", + "inputs": {"language": "typescript", "framework": "zod"}, + "order": 3 + } + ] + elif "api.analyzer" in agent_name: + skills_to_invoke = [ + { + "skill": "api.validate", + "purpose": "Analyze API specification for issues and best practices", + "inputs": {"include_warnings": True}, + "order": 1 + }, + { + "skill": "api.compatibility", + "purpose": "Check compatibility with existing APIs", + "inputs": {"check_breaking_changes": True}, + "order": 2 + } + ] + else: + # Generic response - extract skills from prompt + if "AVAILABLE SKILLS" in prompt: + skills_section_start = prompt.index("AVAILABLE SKILLS") + skills_section_end = prompt.index("##", skills_section_start + 10) if prompt.count("##", skills_section_start) > 0 else len(prompt) + skills_text = prompt[skills_section_start:skills_section_end] + + import re + skill_names = re.findall(r'• (\S+)', skills_text) + + for i, skill_name in enumerate(skill_names[:3], 1): + skills_to_invoke.append({ + "skill": skill_name, + "purpose": f"Execute {skill_name} as part of agent workflow", + "inputs": {}, + "order": i + }) + + response = { + "analysis": f"As {agent_name}, I will approach this task using my available skills in a structured sequence.", + "skills_to_invoke": skills_to_invoke, + "reasoning": "Selected skills follow the agent's workflow pattern and capabilities.", + "mode": "simulated", + "note": "This is a mock response. In production, Claude API would provide real analysis." + } + + return response + + +def execute_skills( + skills_plan: List[Dict[str, Any]], + reasoning_mode: str +) -> List[Dict[str, Any]]: + """ + Execute the planned skills (currently simulated). + + In production, this would: + 1. For each skill in the plan: + - Load the skill manifest + - Prepare inputs + - Execute the skill handler + - Capture output + 2. In iterative mode: analyze results and potentially invoke more skills + + Args: + skills_plan: List of skills to invoke with their inputs + reasoning_mode: 'iterative' or 'oneshot' + + Returns: + List of execution results + """ + results = [] + + for skill_info in skills_plan: + execution_result = { + "skill": skill_info.get("skill"), + "purpose": skill_info.get("purpose"), + "status": "simulated", + "timestamp": datetime.now(timezone.utc).isoformat(), + "output": { + "note": f"Simulated execution of {skill_info.get('skill')}", + "inputs": skill_info.get("inputs", {}), + "success": True + } + } + + results.append(execution_result) + + # In iterative mode, we might make decisions based on results + if reasoning_mode == "iterative": + execution_result["iterative_note"] = ( + "In iterative mode, the agent would analyze this result " + "and potentially invoke additional skills or retry." + ) + + return results + + +def save_execution_log( + agent_name: str, + execution_data: Dict[str, Any] +) -> str: + """ + Save execution log to agent_logs/.json + + Args: + agent_name: Name of the agent + execution_data: Complete execution data to log + + Returns: + Path to the saved log file + """ + # Ensure logs directory exists + os.makedirs(AGENT_LOGS_DIR, exist_ok=True) + + # Generate log filename with timestamp + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") + log_filename = f"{agent_name}_{timestamp}.json" + log_path = os.path.join(AGENT_LOGS_DIR, log_filename) + + # Also maintain a "latest" symlink + latest_path = os.path.join(AGENT_LOGS_DIR, f"{agent_name}_latest.json") + + try: + with open(log_path, 'w') as f: + json.dump(execution_data, f, indent=2) + + # Create/update latest symlink + if os.path.exists(latest_path): + os.remove(latest_path) + os.symlink(os.path.basename(log_path), latest_path) + + logger.info(f"Execution log saved to {log_path}") + return log_path + except Exception as e: + logger.error(f"Failed to save execution log: {e}") + raise BettyError(f"Failed to save execution log: {e}") + + +def run_agent( + agent_path: str, + task_context: Optional[str] = None, + save_log: bool = True +) -> Dict[str, Any]: + """ + Execute a Betty agent. + + Args: + agent_path: Path to agent manifest or agent name + task_context: User-provided task or query + save_log: Whether to save execution log to disk + + Returns: + Execution result dictionary + """ + logger.info(f"Running agent: {agent_path}") + + # Track execution time for telemetry + start_time = datetime.now(timezone.utc) + + try: + # Load agent manifest + agent_manifest = load_agent_manifest(agent_path) + agent_name = agent_manifest.get("name") + reasoning_mode = agent_manifest.get("reasoning_mode", "oneshot") + + logger.info(f"Loaded agent: {agent_name} (mode: {reasoning_mode})") + + # Load skill registry + skill_registry = load_skill_registry() + + # Validate that agent's skills are available + skills_available = agent_manifest.get("skills_available", []) + skills_info = [] + missing_skills = [] + + for skill_name in skills_available: + skill_info = get_skill_info(skill_name, skill_registry) + if skill_info: + skills_info.append({ + "name": skill_name, + "description": skill_info.get("description", ""), + "status": skill_info.get("status", "unknown") + }) + else: + missing_skills.append(skill_name) + logger.warning(f"Skill not found in registry: {skill_name}") + + # Construct agent prompt + logger.info("Constructing agent prompt...") + prompt = construct_agent_prompt(agent_manifest, task_context) + + # Call Claude API (or mock) + logger.info("Invoking Claude API...") + claude_response = call_claude_api(prompt, agent_name) + + # Execute skills based on Claude's plan + skills_plan = claude_response.get("skills_to_invoke", []) + logger.info(f"Executing {len(skills_plan)} skills...") + execution_results = execute_skills(skills_plan, reasoning_mode) + + # Build complete execution data + execution_data = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "agent": { + "name": agent_name, + "version": agent_manifest.get("version"), + "description": agent_manifest.get("description"), + "reasoning_mode": reasoning_mode, + "status": agent_manifest.get("status", "unknown") + }, + "task_context": task_context or "No task provided", + "prompt": prompt, + "skills_available": skills_info, + "missing_skills": missing_skills, + "claude_response": claude_response, + "execution_results": execution_results, + "summary": { + "skills_planned": len(skills_plan), + "skills_executed": len(execution_results), + "success": all(r.get("output", {}).get("success", False) for r in execution_results) + } + } + + # Save log if requested + log_path = None + if save_log: + log_path = save_execution_log(agent_name, execution_data) + execution_data["log_path"] = log_path + + # Calculate execution duration + end_time = datetime.now(timezone.utc) + duration_ms = int((end_time - start_time).total_seconds() * 1000) + + # Capture telemetry for successful agent execution + capture_skill_execution( + skill_name="agent.run", + inputs={ + "agent": agent_name, + "task_context": task_context or "No task provided", + }, + status="success" if execution_data["summary"]["success"] else "failed", + duration_ms=duration_ms, + agent=agent_name, + caller="cli", + reasoning_mode=reasoning_mode, + skills_planned=len(skills_plan), + skills_executed=len(execution_results), + ) + + # Log audit entry for agent execution + capture_audit_entry( + skill_name="agent.run", + status="success" if execution_data["summary"]["success"] else "failed", + duration_ms=duration_ms, + errors=None, + metadata={ + "agent": agent_name, + "reasoning_mode": reasoning_mode, + "skills_executed": len(execution_results), + "task_context": task_context or "No task provided", + } + ) + + return build_response( + ok=True, + details=execution_data + ) + + except BettyError as e: + logger.error(f"Agent execution failed: {e}") + error_info = format_error_response(e, include_traceback=False) + + # Calculate execution duration for failed case + end_time = datetime.now(timezone.utc) + duration_ms = int((end_time - start_time).total_seconds() * 1000) + + # Capture telemetry for failed agent execution + capture_skill_execution( + skill_name="agent.run", + inputs={"agent_path": agent_path}, + status="failed", + duration_ms=duration_ms, + caller="cli", + error=str(e), + ) + + # Log audit entry for failed agent execution + capture_audit_entry( + skill_name="agent.run", + status="failed", + duration_ms=duration_ms, + errors=[str(e)], + metadata={ + "agent_path": agent_path, + "error_type": "BettyError", + } + ) + + return build_response( + ok=False, + errors=[str(e)], + details={"error": error_info} + ) + except Exception as e: + logger.error(f"Unexpected error: {e}", exc_info=True) + error_info = format_error_response(e, include_traceback=True) + + # Calculate execution duration for failed case + end_time = datetime.now(timezone.utc) + duration_ms = int((end_time - start_time).total_seconds() * 1000) + + # Capture telemetry for unexpected error + capture_skill_execution( + skill_name="agent.run", + inputs={"agent_path": agent_path}, + status="failed", + duration_ms=duration_ms, + caller="cli", + error=str(e), + ) + + # Log audit entry for unexpected error + capture_audit_entry( + skill_name="agent.run", + status="failed", + duration_ms=duration_ms, + errors=[f"Unexpected error: {str(e)}"], + metadata={ + "agent_path": agent_path, + "error_type": type(e).__name__, + } + ) + + return build_response( + ok=False, + errors=[f"Unexpected error: {str(e)}"], + details={"error": error_info} + ) + + +@capture_telemetry(skill_name="agent.run", caller="cli") +def main(): + """Main CLI entry point.""" + if len(sys.argv) < 2: + message = "Usage: agent_run.py [task_context] [--no-save-log]" + response = build_response( + False, + errors=[message], + details={ + "usage": message, + "examples": [ + "agent_run.py api.designer", + "agent_run.py api.designer 'Create API for user management'", + "agent_run.py agents/api.designer/agent.yaml 'Design REST API'" + ] + } + ) + print(json.dumps(response, indent=2), file=sys.stderr) + sys.exit(1) + + agent_path = sys.argv[1] + + # Parse optional arguments + task_context = None + save_log = True + + for arg in sys.argv[2:]: + if arg == "--no-save-log": + save_log = False + elif task_context is None: + task_context = arg + + try: + result = run_agent(agent_path, task_context, save_log) + + # Check if execution was successful + if result['ok'] and 'details' in result and 'agent' in result['details']: + # Pretty print for CLI usage + print("\n" + "="*80) + print(f"AGENT EXECUTION: {result['details']['agent']['name']}") + print("="*80) + + agent_info = result['details']['agent'] + print(f"\nAgent: {agent_info['name']} v{agent_info['version']}") + print(f"Mode: {agent_info['reasoning_mode']}") + print(f"Status: {agent_info['status']}") + + print(f"\nTask: {result['details']['task_context']}") + + print("\n" + "-"*80) + print("CLAUDE RESPONSE:") + print("-"*80) + print(json.dumps(result['details']['claude_response'], indent=2)) + + print("\n" + "-"*80) + print("EXECUTION RESULTS:") + print("-"*80) + for exec_result in result['details']['execution_results']: + print(f"\n ✓ {exec_result['skill']}") + print(f" Purpose: {exec_result['purpose']}") + print(f" Status: {exec_result['status']}") + + if 'log_path' in result['details']: + print(f"\n📝 Log saved to: {result['details']['log_path']}") + + print("\n" + "="*80) + print("EXECUTION COMPLETE") + print("="*80 + "\n") + else: + # Execution failed - print error details + print("\n" + "="*80) + print("AGENT EXECUTION FAILED") + print("="*80) + print(f"\nErrors:") + for error in result.get('errors', ['Unknown error']): + print(f" ✗ {error}") + print() + + # Also output full JSON for programmatic use + print(json.dumps(result, indent=2)) + sys.exit(0 if result['ok'] else 1) + + except KeyboardInterrupt: + print("\n\nInterrupted by user", file=sys.stderr) + sys.exit(130) + + +if __name__ == "__main__": + main() diff --git a/skills/agent.run/skill.yaml b/skills/agent.run/skill.yaml new file mode 100644 index 0000000..78938d2 --- /dev/null +++ b/skills/agent.run/skill.yaml @@ -0,0 +1,85 @@ +name: agent.run +version: 0.1.0 +description: > + Execute a registered Betty agent by loading its manifest, generating a Claude-friendly + prompt, invoking skills based on the agent's workflow, and logging results. Supports + both iterative and oneshot reasoning modes with optional Claude API integration. + +inputs: + - name: agent_path + type: string + required: true + description: Path to agent manifest (agent.yaml) or agent name (e.g., api.designer) + + - name: task_context + type: string + required: false + description: Task or query to provide to the agent for execution + + - name: save_log + type: boolean + required: false + default: true + description: Whether to save execution log to agent_logs/.json + +outputs: + - name: execution_result + type: object + description: Complete execution results including prompt, Claude response, and skill outputs + schema: + properties: + ok: boolean + status: string + timestamp: string + errors: array + details: + type: object + properties: + timestamp: string + agent: object + task_context: string + prompt: string + skills_available: array + claude_response: object + execution_results: array + summary: object + log_path: string + +dependencies: + - agent.define + +entrypoints: + - command: /agent/run + handler: agent_run.py + runtime: python + description: > + Execute a Betty agent with optional task context. Generates Claude-friendly prompts, + invokes the Claude API (or simulates), executes planned skills, and logs all results + to agent_logs/ directory. + parameters: + - name: agent_path + type: string + required: true + description: Path to agent.yaml file or agent name (e.g., api.designer) + - name: task_context + type: string + required: false + description: Optional task or query for the agent to execute + - name: save_log + type: boolean + required: false + default: true + description: Save execution log to agent_logs/_.json + permissions: + - filesystem:read + - filesystem:write + - network:http + +status: active + +tags: + - agents + - execution + - claude-api + - orchestration + - layer2 diff --git a/skills/api.compatibility/SKILL.md b/skills/api.compatibility/SKILL.md new file mode 100644 index 0000000..61714e6 --- /dev/null +++ b/skills/api.compatibility/SKILL.md @@ -0,0 +1,46 @@ +# api.compatibility + +## Overview + +Detect breaking changes between API specification versions to maintain backward compatibility. + +## Usage + +```bash +python skills/api.compatibility/check_compatibility.py [options] +``` + +## Examples + +```bash +# Check compatibility +python skills/api.compatibility/check_compatibility.py \ + specs/user-service-v1.openapi.yaml \ + specs/user-service-v2.openapi.yaml + +# Human-readable output +python skills/api.compatibility/check_compatibility.py \ + specs/user-service-v1.openapi.yaml \ + specs/user-service-v2.openapi.yaml \ + --format=human +``` + +## Breaking Changes Detected + +- **path_removed**: Endpoint removed +- **operation_removed**: HTTP method removed +- **schema_removed**: Model schema removed +- **property_removed**: Schema property removed +- **property_made_required**: Optional property now required +- **property_type_changed**: Property type changed + +## Non-Breaking Changes + +- **path_added**: New endpoint +- **operation_added**: New HTTP method +- **schema_added**: New model schema +- **property_added**: New optional property + +## Version + +**0.1.0** - Initial implementation diff --git a/skills/api.compatibility/__init__.py b/skills/api.compatibility/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/api.compatibility/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/api.compatibility/check_compatibility.py b/skills/api.compatibility/check_compatibility.py new file mode 100755 index 0000000..e2a9972 --- /dev/null +++ b/skills/api.compatibility/check_compatibility.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python3 +""" +Detect breaking changes between API specification versions. + +This skill analyzes two versions of an API spec and identifies: +- Breaking changes (remove endpoints, change types, etc.) +- Non-breaking changes (add endpoints, add optional fields, etc.) +""" + +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, Any, List, Tuple + +# Add betty module to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response, BettyError +from betty.validation import validate_path + +logger = setup_logger(__name__) + + +class CompatibilityChange: + """Represents a compatibility change between spec versions.""" + + def __init__( + self, + change_type: str, + severity: str, + path: str, + description: str, + old_value: Any = None, + new_value: Any = None + ): + self.change_type = change_type + self.severity = severity # "breaking" or "non-breaking" + self.path = path + self.description = description + self.old_value = old_value + self.new_value = new_value + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary.""" + result = { + "change_type": self.change_type, + "severity": self.severity, + "path": self.path, + "description": self.description + } + if self.old_value is not None: + result["old_value"] = self.old_value + if self.new_value is not None: + result["new_value"] = self.new_value + return result + + +class CompatibilityChecker: + """Check compatibility between two API specs.""" + + def __init__(self, old_spec: Dict[str, Any], new_spec: Dict[str, Any]): + self.old_spec = old_spec + self.new_spec = new_spec + self.breaking_changes: List[CompatibilityChange] = [] + self.non_breaking_changes: List[CompatibilityChange] = [] + + def check(self) -> Dict[str, Any]: + """ + Run all compatibility checks. + + Returns: + Compatibility report + """ + # Check paths (endpoints) + self._check_paths() + + # Check schemas + self._check_schemas() + + # Check parameters + self._check_parameters() + + # Check responses + self._check_responses() + + return { + "compatible": len(self.breaking_changes) == 0, + "breaking_changes": [c.to_dict() for c in self.breaking_changes], + "non_breaking_changes": [c.to_dict() for c in self.non_breaking_changes], + "change_summary": { + "total_breaking": len(self.breaking_changes), + "total_non_breaking": len(self.non_breaking_changes), + "total_changes": len(self.breaking_changes) + len(self.non_breaking_changes) + } + } + + def _check_paths(self): + """Check for changes in API paths/endpoints.""" + old_paths = set(self.old_spec.get("paths", {}).keys()) + new_paths = set(self.new_spec.get("paths", {}).keys()) + + # Removed paths (BREAKING) + for removed_path in old_paths - new_paths: + self.breaking_changes.append(CompatibilityChange( + change_type="path_removed", + severity="breaking", + path=f"paths.{removed_path}", + description=f"Endpoint '{removed_path}' was removed", + old_value=removed_path + )) + + # Added paths (NON-BREAKING) + for added_path in new_paths - old_paths: + self.non_breaking_changes.append(CompatibilityChange( + change_type="path_added", + severity="non-breaking", + path=f"paths.{added_path}", + description=f"New endpoint '{added_path}' was added", + new_value=added_path + )) + + # Check operations on existing paths + for path in old_paths & new_paths: + self._check_operations(path) + + def _check_operations(self, path: str): + """Check for changes in HTTP operations on a path.""" + old_operations = set(self.old_spec["paths"][path].keys()) - {"parameters"} + new_operations = set(self.new_spec["paths"][path].keys()) - {"parameters"} + + # Removed operations (BREAKING) + for removed_op in old_operations - new_operations: + self.breaking_changes.append(CompatibilityChange( + change_type="operation_removed", + severity="breaking", + path=f"paths.{path}.{removed_op}", + description=f"Operation '{removed_op.upper()}' on '{path}' was removed", + old_value=removed_op + )) + + # Added operations (NON-BREAKING) + for added_op in new_operations - old_operations: + self.non_breaking_changes.append(CompatibilityChange( + change_type="operation_added", + severity="non-breaking", + path=f"paths.{path}.{added_op}", + description=f"New operation '{added_op.upper()}' on '{path}' was added", + new_value=added_op + )) + + def _check_schemas(self): + """Check for changes in component schemas.""" + old_schemas = self.old_spec.get("components", {}).get("schemas", {}) + new_schemas = self.new_spec.get("components", {}).get("schemas", {}) + + old_schema_names = set(old_schemas.keys()) + new_schema_names = set(new_schemas.keys()) + + # Removed schemas (BREAKING if they were referenced) + for removed_schema in old_schema_names - new_schema_names: + self.breaking_changes.append(CompatibilityChange( + change_type="schema_removed", + severity="breaking", + path=f"components.schemas.{removed_schema}", + description=f"Schema '{removed_schema}' was removed", + old_value=removed_schema + )) + + # Added schemas (NON-BREAKING) + for added_schema in new_schema_names - old_schema_names: + self.non_breaking_changes.append(CompatibilityChange( + change_type="schema_added", + severity="non-breaking", + path=f"components.schemas.{added_schema}", + description=f"New schema '{added_schema}' was added", + new_value=added_schema + )) + + # Check properties on existing schemas + for schema_name in old_schema_names & new_schema_names: + self._check_schema_properties(schema_name, old_schemas[schema_name], new_schemas[schema_name]) + + def _check_schema_properties(self, schema_name: str, old_schema: Dict[str, Any], new_schema: Dict[str, Any]): + """Check for changes in schema properties.""" + old_props = old_schema.get("properties") or {} + new_props = new_schema.get("properties") or {} + + old_required = set(old_schema.get("required", [])) + new_required = set(new_schema.get("required", [])) + + old_prop_names = set(old_props.keys()) + new_prop_names = set(new_props.keys()) + + # Removed properties (BREAKING) + for removed_prop in old_prop_names - new_prop_names: + self.breaking_changes.append(CompatibilityChange( + change_type="property_removed", + severity="breaking", + path=f"components.schemas.{schema_name}.properties.{removed_prop}", + description=f"Property '{removed_prop}' was removed from schema '{schema_name}'", + old_value=removed_prop + )) + + # Added required properties (BREAKING) + for added_required in new_required - old_required: + if added_required in new_prop_names: + self.breaking_changes.append(CompatibilityChange( + change_type="property_made_required", + severity="breaking", + path=f"components.schemas.{schema_name}.required", + description=f"Property '{added_required}' is now required in schema '{schema_name}'", + new_value=added_required + )) + + # Added optional properties (NON-BREAKING) + for added_prop in new_prop_names - old_prop_names: + if added_prop not in new_required: + self.non_breaking_changes.append(CompatibilityChange( + change_type="property_added", + severity="non-breaking", + path=f"components.schemas.{schema_name}.properties.{added_prop}", + description=f"Optional property '{added_prop}' was added to schema '{schema_name}'", + new_value=added_prop + )) + + # Check for type changes on existing properties + for prop_name in old_prop_names & new_prop_names: + old_type = old_props[prop_name].get("type") + new_type = new_props[prop_name].get("type") + + if old_type != new_type: + self.breaking_changes.append(CompatibilityChange( + change_type="property_type_changed", + severity="breaking", + path=f"components.schemas.{schema_name}.properties.{prop_name}.type", + description=f"Property '{prop_name}' type changed from '{old_type}' to '{new_type}' in schema '{schema_name}'", + old_value=old_type, + new_value=new_type + )) + + def _check_parameters(self): + """Check for changes in path/query parameters.""" + # Implementation for parameter checking + pass + + def _check_responses(self): + """Check for changes in response schemas.""" + # Implementation for response checking + pass + + +def load_spec(spec_path: str) -> Dict[str, Any]: + """ + Load API specification from file. + + Args: + spec_path: Path to specification file + + Returns: + Parsed specification + + Raises: + BettyError: If file cannot be loaded + """ + spec_file = Path(spec_path) + + if not spec_file.exists(): + raise BettyError(f"Specification file not found: {spec_path}") + + try: + import yaml + with open(spec_file, 'r') as f: + spec = yaml.safe_load(f) + + if not isinstance(spec, dict): + raise BettyError("Specification must be a valid YAML/JSON object") + + logger.info(f"Loaded specification from {spec_path}") + return spec + + except Exception as e: + raise BettyError(f"Failed to load specification: {e}") + + +def check_compatibility( + old_spec_path: str, + new_spec_path: str, + fail_on_breaking: bool = True +) -> Dict[str, Any]: + """ + Check compatibility between two API specifications. + + Args: + old_spec_path: Path to old specification + new_spec_path: Path to new specification + fail_on_breaking: Whether to fail if breaking changes detected + + Returns: + Compatibility report + + Raises: + BettyError: If compatibility check fails + """ + # Load specifications + old_spec = load_spec(old_spec_path) + new_spec = load_spec(new_spec_path) + + # Run compatibility check + checker = CompatibilityChecker(old_spec, new_spec) + report = checker.check() + + # Add metadata + report["old_spec_path"] = old_spec_path + report["new_spec_path"] = new_spec_path + + return report + + +def format_compatibility_output(report: Dict[str, Any]) -> str: + """Format compatibility report for human-readable output.""" + lines = [] + + lines.append("\n" + "=" * 60) + lines.append("API Compatibility Report") + lines.append("=" * 60) + lines.append(f"Old: {report.get('old_spec_path', 'unknown')}") + lines.append(f"New: {report.get('new_spec_path', 'unknown')}") + lines.append("=" * 60 + "\n") + + # Breaking changes + breaking = report.get("breaking_changes", []) + if breaking: + lines.append(f"❌ BREAKING CHANGES ({len(breaking)}):") + for change in breaking: + lines.append(f" [{change.get('change_type', 'UNKNOWN')}] {change.get('description', '')}") + if change.get('path'): + lines.append(f" Path: {change['path']}") + lines.append("") + + # Non-breaking changes + non_breaking = report.get("non_breaking_changes", []) + if non_breaking: + lines.append(f"✅ NON-BREAKING CHANGES ({len(non_breaking)}):") + for change in non_breaking: + lines.append(f" [{change.get('change_type', 'UNKNOWN')}] {change.get('description', '')}") + lines.append("") + + # Summary + lines.append("=" * 60) + if report.get("compatible"): + lines.append("✅ BACKWARD COMPATIBLE") + else: + lines.append("❌ NOT BACKWARD COMPATIBLE") + lines.append("=" * 60 + "\n") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="Detect breaking changes between API specification versions" + ) + parser.add_argument( + "old_spec_path", + type=str, + help="Path to the old/previous API specification" + ) + parser.add_argument( + "new_spec_path", + type=str, + help="Path to the new/current API specification" + ) + parser.add_argument( + "--fail-on-breaking", + action="store_true", + default=True, + help="Exit with error code if breaking changes detected (default: true)" + ) + parser.add_argument( + "--format", + type=str, + choices=["json", "human"], + default="json", + help="Output format (default: json)" + ) + + args = parser.parse_args() + + try: + # Check if PyYAML is installed + try: + import yaml + except ImportError: + raise BettyError( + "PyYAML is required for api.compatibility. Install with: pip install pyyaml" + ) + + # Validate inputs + validate_path(args.old_spec_path) + validate_path(args.new_spec_path) + + # Run compatibility check + logger.info(f"Checking compatibility between {args.old_spec_path} and {args.new_spec_path}") + report = check_compatibility( + old_spec_path=args.old_spec_path, + new_spec_path=args.new_spec_path, + fail_on_breaking=args.fail_on_breaking + ) + + # Output based on format + if args.format == "human": + print(format_compatibility_output(report)) + else: + output = { + "status": "success", + "data": report + } + print(json.dumps(output, indent=2)) + + # Exit with error if breaking changes and fail_on_breaking is True + if args.fail_on_breaking and not report["compatible"]: + sys.exit(1) + + except Exception as e: + logger.error(f"Compatibility check failed: {e}") + print(json.dumps(format_error_response(e), indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/api.compatibility/skill.yaml b/skills/api.compatibility/skill.yaml new file mode 100644 index 0000000..90de333 --- /dev/null +++ b/skills/api.compatibility/skill.yaml @@ -0,0 +1,51 @@ +name: api.compatibility +version: 0.1.0 +description: Detect breaking changes between API specification versions + +inputs: + - name: old_spec_path + type: string + required: true + description: Path to the old/previous API specification + + - name: new_spec_path + type: string + required: true + description: Path to the new/current API specification + + - name: fail_on_breaking + type: boolean + required: false + default: true + description: Exit with error code if breaking changes detected + +outputs: + - name: compatible + type: boolean + description: Whether the new spec is backward compatible + + - name: breaking_changes + type: array + description: List of breaking changes detected + + - name: non_breaking_changes + type: array + description: List of non-breaking changes detected + + - name: change_summary + type: object + description: Summary of all changes + +dependencies: + - context.schema + +entrypoints: + - command: /skill/api/compatibility + handler: check_compatibility.py + runtime: python + permissions: + - filesystem:read + +status: active + +tags: [api, compatibility, breaking-changes, versioning, openapi] diff --git a/skills/api.define/SKILL.md b/skills/api.define/SKILL.md new file mode 100644 index 0000000..0e2f87a --- /dev/null +++ b/skills/api.define/SKILL.md @@ -0,0 +1,231 @@ +# api.define + +## Overview + +**api.define** scaffolds OpenAPI and AsyncAPI specifications from enterprise-compliant templates, generating production-ready API contracts with best practices built-in. + +## Purpose + +Quickly create API specifications that follow enterprise guidelines: +- Generate Zalando-compliant OpenAPI 3.1 specs +- Generate AsyncAPI 3.0 specs for event-driven APIs +- Include proper error handling (RFC 7807 Problem JSON) +- Use correct naming conventions (snake_case) +- Include required metadata and security schemes + +## Usage + +### Basic Usage + +```bash +python skills/api.define/api_define.py [spec_type] [options] +``` + +### Parameters + +| Parameter | Required | Description | Default | +|-----------|----------|-------------|---------| +| `service_name` | Yes | Service/API name | - | +| `spec_type` | No | openapi or asyncapi | `openapi` | +| `--template` | No | Template name | `zalando` | +| `--output-dir` | No | Output directory | `specs` | +| `--version` | No | API version | `1.0.0` | + +## Examples + +### Example 1: Create Zalando-Compliant OpenAPI Spec + +```bash +python skills/api.define/api_define.py user-service openapi --template=zalando +``` + +**Output**: `specs/user-service.openapi.yaml` + +Generated spec includes: +- ✅ Required Zalando metadata (`x-api-id`, `x-audience`) +- ✅ CRUD operations for users resource +- ✅ RFC 7807 Problem JSON for errors +- ✅ snake_case property names +- ✅ X-Flow-ID headers for tracing +- ✅ Proper HTTP status codes +- ✅ JWT authentication scheme + +### Example 2: Create AsyncAPI Spec + +```bash +python skills/api.define/api_define.py user-service asyncapi +``` + +**Output**: `specs/user-service.asyncapi.yaml` + +Generated spec includes: +- ✅ Lifecycle events (created, updated, deleted) +- ✅ Kafka channel definitions +- ✅ Event payload schemas +- ✅ Publish/subscribe operations + +### Example 3: Custom Output Directory + +```bash +python skills/api.define/api_define.py order-api openapi \ + --output-dir=api-specs \ + --version=2.0.0 +``` + +## Generated OpenAPI Structure + +For a service named `user-service`, the generated OpenAPI spec includes: + +**Paths**: +- `GET /users` - List users with pagination +- `POST /users` - Create new user +- `GET /users/{user_id}` - Get user by ID +- `PUT /users/{user_id}` - Update user +- `DELETE /users/{user_id}` - Delete user + +**Schemas**: +- `User` - Main resource schema +- `UserCreate` - Creation payload schema +- `UserUpdate` - Update payload schema +- `Pagination` - Pagination metadata +- `Problem` - RFC 7807 error schema + +**Responses**: +- `200` - Success (with X-Flow-ID header) +- `201` - Created (with Location and X-Flow-ID headers) +- `204` - No Content (for deletes) +- `400` - Bad Request (application/problem+json) +- `404` - Not Found (application/problem+json) +- `409` - Conflict (application/problem+json) +- `500` - Internal Error (application/problem+json) + +**Security**: +- Bearer token authentication (JWT) + +**Required Metadata**: +- `x-api-id` - Unique UUID +- `x-audience` - Target audience (company-internal) +- `contact` - Team contact information + +## Resource Name Extraction + +The skill automatically extracts resource names from service names: + +| Service Name | Resource | Plural | +|--------------|----------|--------| +| `user-service` | user | users | +| `order-api` | order | orders | +| `payment-gateway` | payment | payments | + +## Naming Conventions + +The skill automatically applies proper naming: + +| Context | Convention | Example | +|---------|------------|---------| +| Paths | kebab-case | `/user-profiles` | +| Properties | snake_case | `user_id` | +| Schemas | TitleCase | `UserProfile` | +| Operations | camelCase | `getUserById` | + +## Integration with api.validate + +The generated specs are designed to pass `api.validate` with zero errors: + +```bash +# Generate spec +python skills/api.define/api_define.py user-service + +# Validate (should pass) +python skills/api.validate/api_validate.py specs/user-service.openapi.yaml zalando +``` + +## Use in Workflows + +```yaml +# workflows/api_first_development.yaml +steps: + - skill: api.define + args: + - "user-service" + - "openapi" + - "--template=zalando" + output: spec_path + + - skill: api.validate + args: + - "{spec_path}" + - "zalando" + required: true +``` + +## Customization + +After generation, customize the spec: + +1. **Add properties** to schemas: +```yaml +User: + properties: + user_id: ... + email: # Add this + type: string + format: email +``` + +2. **Add operations**: +```yaml +/users/search: # Add new endpoint + post: + summary: Search users +``` + +3. **Modify metadata**: +```yaml +info: + contact: + name: Your Team Name # Update this + email: team@company.com +``` + +## Output + +### Success Response + +```json +{ + "status": "success", + "data": { + "spec_path": "specs/user-service.openapi.yaml", + "spec_content": {...}, + "api_id": "d0184f38-b98d-11e7-9c56-68f728c1ba70", + "template_used": "zalando", + "service_name": "user-service" + } +} +``` + +## Dependencies + +- **PyYAML**: Required for YAML handling + ```bash + pip install pyyaml + ``` + +## Templates + +| Template | Spec Type | Description | +|----------|-----------|-------------| +| `zalando` | OpenAPI | Zalando-compliant with all required fields | +| `basic` | AsyncAPI | Basic event-driven API structure | + +## See Also + +- [api.validate](../api.validate/SKILL.md) - Validate generated specs +- [hook.define](../hook.define/SKILL.md) - Set up automatic validation +- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model +- [API-Driven Development](../../docs/api-driven-development.md) - Complete guide + +## Version + +**0.1.0** - Initial implementation with Zalando template support diff --git a/skills/api.define/__init__.py b/skills/api.define/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/api.define/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/api.define/api_define.py b/skills/api.define/api_define.py new file mode 100755 index 0000000..7e8d3a0 --- /dev/null +++ b/skills/api.define/api_define.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +""" +Create OpenAPI and AsyncAPI specifications from templates. + +This skill scaffolds API specifications following enterprise guidelines +with proper structure and best practices built-in. +""" + +import sys +import json +import argparse +import uuid +import re +from pathlib import Path +from typing import Dict, Any + +# Add betty module to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response, BettyError +from betty.validation import validate_skill_name + +logger = setup_logger(__name__) + + +def to_snake_case(text: str) -> str: + """Convert text to snake_case.""" + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) + s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) + return s2.lower().replace('-', '_').replace(' ', '_') + + +def to_kebab_case(text: str) -> str: + """Convert text to kebab-case.""" + return to_snake_case(text).replace('_', '-') + + +def to_title_case(text: str) -> str: + """Convert text to TitleCase.""" + return ''.join(word.capitalize() for word in re.split(r'[-_\s]', text)) + + +def pluralize(word: str) -> str: + """Simple pluralization (works for most common cases).""" + if word.endswith('y'): + return word[:-1] + 'ies' + elif word.endswith('s'): + return word + 'es' + else: + return word + 's' + + +def load_template(template_name: str, spec_type: str) -> str: + """ + Load template file content. + + Args: + template_name: Template name (zalando, basic, minimal) + spec_type: Specification type (openapi or asyncapi) + + Returns: + Template content as string + + Raises: + BettyError: If template not found + """ + template_file = Path(__file__).parent / "templates" / f"{spec_type}-{template_name}.yaml" + + if not template_file.exists(): + raise BettyError( + f"Template not found: {spec_type}-{template_name}.yaml. " + f"Available templates in {template_file.parent}: " + f"{', '.join([f.stem for f in template_file.parent.glob(f'{spec_type}-*.yaml')])}" + ) + + try: + with open(template_file, 'r') as f: + content = f.read() + logger.info(f"Loaded template: {template_file}") + return content + except Exception as e: + raise BettyError(f"Failed to load template: {e}") + + +def render_template(template: str, variables: Dict[str, str]) -> str: + """ + Render template with variables. + + Args: + template: Template string with {{variable}} placeholders + variables: Variable values to substitute + + Returns: + Rendered template string + """ + result = template + for key, value in variables.items(): + placeholder = f"{{{{{key}}}}}" + result = result.replace(placeholder, str(value)) + + # Check for unrendered variables + unrendered = re.findall(r'\{\{(\w+)\}\}', result) + if unrendered: + logger.warning(f"Unrendered template variables: {', '.join(set(unrendered))}") + + return result + + +def extract_resource_name(service_name: str) -> str: + """ + Extract primary resource name from service name. + + Examples: + user-service -> user + order-api -> order + payment-gateway -> payment + """ + # Remove common suffixes + for suffix in ['-service', '-api', '-gateway', '-manager']: + if service_name.endswith(suffix): + return service_name[:-len(suffix)] + + return service_name + + +def generate_openapi_spec( + service_name: str, + template_name: str = "zalando", + version: str = "1.0.0", + output_dir: str = "specs" +) -> Dict[str, Any]: + """ + Generate OpenAPI specification from template. + + Args: + service_name: Service/API name + template_name: Template to use + version: API version + output_dir: Output directory + + Returns: + Result dictionary with spec path and content + """ + # Generate API ID + api_id = str(uuid.uuid4()) + + # Extract resource name + resource_name = extract_resource_name(service_name) + + # Generate template variables + variables = { + "service_name": to_kebab_case(service_name), + "service_title": to_title_case(service_name), + "version": version, + "description": f"RESTful API for {service_name.replace('-', ' ')} management", + "team_name": "Platform Team", + "team_email": "platform@company.com", + "api_id": api_id, + "audience": "company-internal", + "resource_singular": to_snake_case(resource_name), + "resource_plural": pluralize(to_snake_case(resource_name)), + "resource_title": to_title_case(resource_name), + "resource_schema": to_title_case(resource_name) + } + + logger.info(f"Generated template variables: {variables}") + + # Load and render template + template = load_template(template_name, "openapi") + spec_content = render_template(template, variables) + + # Parse to validate YAML + try: + import yaml + spec_dict = yaml.safe_load(spec_content) + except Exception as e: + raise BettyError(f"Failed to parse generated spec: {e}") + + # Create output directory + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + # Write specification file + spec_filename = f"{to_kebab_case(service_name)}.openapi.yaml" + spec_path = output_path / spec_filename + + with open(spec_path, 'w') as f: + f.write(spec_content) + + logger.info(f"Generated OpenAPI spec: {spec_path}") + + return { + "spec_path": str(spec_path), + "spec_content": spec_dict, + "api_id": api_id, + "template_used": template_name, + "service_name": to_kebab_case(service_name) + } + + +def generate_asyncapi_spec( + service_name: str, + template_name: str = "basic", + version: str = "1.0.0", + output_dir: str = "specs" +) -> Dict[str, Any]: + """ + Generate AsyncAPI specification from template. + + Args: + service_name: Service/API name + template_name: Template to use + version: API version + output_dir: Output directory + + Returns: + Result dictionary with spec path and content + """ + # Basic AsyncAPI template (inline for now) + resource_name = extract_resource_name(service_name) + + asyncapi_template = f"""asyncapi: 3.0.0 + +info: + title: {to_title_case(service_name)} Events + version: {version} + description: Event-driven API for {service_name.replace('-', ' ')} lifecycle notifications + +servers: + production: + host: kafka.company.com:9092 + protocol: kafka + description: Production Kafka cluster + +channels: + {to_snake_case(resource_name)}.created: + address: {to_snake_case(resource_name)}.created.v1 + messages: + {to_title_case(resource_name)}Created: + $ref: '#/components/messages/{to_title_case(resource_name)}Created' + + {to_snake_case(resource_name)}.updated: + address: {to_snake_case(resource_name)}.updated.v1 + messages: + {to_title_case(resource_name)}Updated: + $ref: '#/components/messages/{to_title_case(resource_name)}Updated' + + {to_snake_case(resource_name)}.deleted: + address: {to_snake_case(resource_name)}.deleted.v1 + messages: + {to_title_case(resource_name)}Deleted: + $ref: '#/components/messages/{to_title_case(resource_name)}Deleted' + +operations: + publish{to_title_case(resource_name)}Created: + action: send + channel: + $ref: '#/channels/{to_snake_case(resource_name)}.created' + + subscribe{to_title_case(resource_name)}Created: + action: receive + channel: + $ref: '#/channels/{to_snake_case(resource_name)}.created' + +components: + messages: + {to_title_case(resource_name)}Created: + name: {to_title_case(resource_name)}Created + title: {to_title_case(resource_name)} Created Event + contentType: application/json + payload: + $ref: '#/components/schemas/{to_title_case(resource_name)}CreatedPayload' + + {to_title_case(resource_name)}Updated: + name: {to_title_case(resource_name)}Updated + title: {to_title_case(resource_name)} Updated Event + contentType: application/json + payload: + $ref: '#/components/schemas/{to_title_case(resource_name)}UpdatedPayload' + + {to_title_case(resource_name)}Deleted: + name: {to_title_case(resource_name)}Deleted + title: {to_title_case(resource_name)} Deleted Event + contentType: application/json + payload: + $ref: '#/components/schemas/{to_title_case(resource_name)}DeletedPayload' + + schemas: + {to_title_case(resource_name)}CreatedPayload: + type: object + required: [event_id, {to_snake_case(resource_name)}_id, occurred_at] + properties: + event_id: + type: string + format: uuid + {to_snake_case(resource_name)}_id: + type: string + format: uuid + occurred_at: + type: string + format: date-time + + {to_title_case(resource_name)}UpdatedPayload: + type: object + required: [event_id, {to_snake_case(resource_name)}_id, occurred_at, changes] + properties: + event_id: + type: string + format: uuid + {to_snake_case(resource_name)}_id: + type: string + format: uuid + changes: + type: object + occurred_at: + type: string + format: date-time + + {to_title_case(resource_name)}DeletedPayload: + type: object + required: [event_id, {to_snake_case(resource_name)}_id, occurred_at] + properties: + event_id: + type: string + format: uuid + {to_snake_case(resource_name)}_id: + type: string + format: uuid + occurred_at: + type: string + format: date-time +""" + + # Parse to validate YAML + try: + import yaml + spec_dict = yaml.safe_load(asyncapi_template) + except Exception as e: + raise BettyError(f"Failed to parse generated spec: {e}") + + # Create output directory + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + # Write specification file + spec_filename = f"{to_kebab_case(service_name)}.asyncapi.yaml" + spec_path = output_path / spec_filename + + with open(spec_path, 'w') as f: + f.write(asyncapi_template) + + logger.info(f"Generated AsyncAPI spec: {spec_path}") + + return { + "spec_path": str(spec_path), + "spec_content": spec_dict, + "template_used": template_name, + "service_name": to_kebab_case(service_name) + } + + +def main(): + parser = argparse.ArgumentParser( + description="Create OpenAPI and AsyncAPI specifications from templates" + ) + parser.add_argument( + "service_name", + type=str, + help="Name of the service/API (e.g., user-service, order-api)" + ) + parser.add_argument( + "spec_type", + type=str, + nargs="?", + default="openapi", + choices=["openapi", "asyncapi"], + help="Type of specification (default: openapi)" + ) + parser.add_argument( + "--template", + type=str, + default="zalando", + help="Template to use (default: zalando for OpenAPI, basic for AsyncAPI)" + ) + parser.add_argument( + "--output-dir", + type=str, + default="specs", + help="Output directory (default: specs)" + ) + parser.add_argument( + "--version", + type=str, + default="1.0.0", + help="API version (default: 1.0.0)" + ) + + args = parser.parse_args() + + try: + # Check if PyYAML is installed + try: + import yaml + except ImportError: + raise BettyError( + "PyYAML is required for api.define. Install with: pip install pyyaml" + ) + + # Generate specification + logger.info( + f"Generating {args.spec_type.upper()} spec for '{args.service_name}' " + f"using template '{args.template}'" + ) + + if args.spec_type == "openapi": + result = generate_openapi_spec( + service_name=args.service_name, + template_name=args.template, + version=args.version, + output_dir=args.output_dir + ) + elif args.spec_type == "asyncapi": + result = generate_asyncapi_spec( + service_name=args.service_name, + template_name=args.template, + version=args.version, + output_dir=args.output_dir + ) + else: + raise BettyError(f"Unsupported spec type: {args.spec_type}") + + # Return structured result + output = { + "status": "success", + "data": result + } + print(json.dumps(output, indent=2)) + + except Exception as e: + logger.error(f"Failed to generate specification: {e}") + print(json.dumps(format_error_response(e), indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/api.define/skill.yaml b/skills/api.define/skill.yaml new file mode 100644 index 0000000..150c584 --- /dev/null +++ b/skills/api.define/skill.yaml @@ -0,0 +1,57 @@ +name: api.define +version: 0.1.0 +description: Create OpenAPI and AsyncAPI specifications from templates + +inputs: + - name: service_name + type: string + required: true + description: Name of the service/API (e.g., user-service, order-api) + + - name: spec_type + type: string + required: false + default: openapi + description: Type of specification (openapi or asyncapi) + + - name: template + type: string + required: false + default: zalando + description: Template to use (zalando, basic, minimal) + + - name: output_dir + type: string + required: false + default: specs + description: Output directory for generated specification + + - name: version + type: string + required: false + default: 1.0.0 + description: API version + +outputs: + - name: spec_path + type: string + description: Path to generated specification file + + - name: spec_content + type: object + description: Generated specification content + +dependencies: + - context.schema + +entrypoints: + - command: /skill/api/define + handler: api_define.py + runtime: python + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: [api, openapi, asyncapi, scaffolding, zalando] diff --git a/skills/api.define/templates/__init__.py b/skills/api.define/templates/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/api.define/templates/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/api.define/templates/openapi-zalando.yaml b/skills/api.define/templates/openapi-zalando.yaml new file mode 100644 index 0000000..17c7831 --- /dev/null +++ b/skills/api.define/templates/openapi-zalando.yaml @@ -0,0 +1,303 @@ +openapi: 3.1.0 + +info: + title: {{service_title}} + version: {{version}} + description: {{description}} + contact: + name: {{team_name}} + email: {{team_email}} + x-api-id: {{api_id}} + x-audience: {{audience}} + +servers: + - url: https://api.company.com/{{service_name}}/v1 + description: Production + +paths: + /{{resource_plural}}: + get: + summary: List {{resource_plural}} + operationId: list{{resource_title}} + tags: [{{resource_title}}] + parameters: + - name: limit + in: query + description: Maximum number of items to return + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: offset + in: query + description: Number of items to skip + schema: + type: integer + minimum: 0 + default: 0 + responses: + '200': + description: List of {{resource_plural}} + headers: + X-Flow-ID: + description: Request flow ID for tracing + schema: + type: string + format: uuid + content: + application/json: + schema: + type: object + required: [{{resource_plural}}, pagination] + properties: + {{resource_plural}}: + type: array + items: + $ref: '#/components/schemas/{{resource_schema}}' + pagination: + $ref: '#/components/schemas/Pagination' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + post: + summary: Create a new {{resource_singular}} + operationId: create{{resource_title}} + tags: [{{resource_title}}] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/{{resource_schema}}Create' + responses: + '201': + description: {{resource_title}} created successfully + headers: + Location: + description: URL of the created resource + schema: + type: string + format: uri + X-Flow-ID: + description: Request flow ID for tracing + schema: + type: string + format: uuid + content: + application/json: + schema: + $ref: '#/components/schemas/{{resource_schema}}' + '400': + $ref: '#/components/responses/BadRequest' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalError' + + /{{resource_plural}}/{{{resource_singular}}_id}: + parameters: + - name: {{resource_singular}}_id + in: path + required: true + description: Unique identifier of the {{resource_singular}} + schema: + type: string + format: uuid + + get: + summary: Get {{resource_singular}} by ID + operationId: get{{resource_title}}ById + tags: [{{resource_title}}] + responses: + '200': + description: {{resource_title}} details + headers: + X-Flow-ID: + description: Request flow ID for tracing + schema: + type: string + format: uuid + content: + application/json: + schema: + $ref: '#/components/schemas/{{resource_schema}}' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + put: + summary: Update {{resource_singular}} + operationId: update{{resource_title}} + tags: [{{resource_title}}] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/{{resource_schema}}Update' + responses: + '200': + description: {{resource_title}} updated successfully + headers: + X-Flow-ID: + description: Request flow ID for tracing + schema: + type: string + format: uuid + content: + application/json: + schema: + $ref: '#/components/schemas/{{resource_schema}}' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + delete: + summary: Delete {{resource_singular}} + operationId: delete{{resource_title}} + tags: [{{resource_title}}] + responses: + '204': + description: {{resource_title}} deleted successfully + headers: + X-Flow-ID: + description: Request flow ID for tracing + schema: + type: string + format: uuid + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + +components: + schemas: + {{resource_schema}}: + type: object + required: [{{resource_singular}}_id, created_at] + properties: + {{resource_singular}}_id: + type: string + format: uuid + description: Unique identifier + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Last update timestamp + + {{resource_schema}}Create: + type: object + required: [] + properties: + # Add creation-specific fields here + + {{resource_schema}}Update: + type: object + properties: + # Add update-specific fields here + + Pagination: + type: object + required: [limit, offset, total] + properties: + limit: + type: integer + description: Number of items per page + offset: + type: integer + description: Number of items skipped + total: + type: integer + description: Total number of items available + + Problem: + type: object + required: [type, title, status] + properties: + type: + type: string + format: uri + description: URI reference identifying the problem type + title: + type: string + description: Short, human-readable summary + status: + type: integer + description: HTTP status code + detail: + type: string + description: Human-readable explanation + instance: + type: string + format: uri + description: URI reference identifying the specific occurrence + + responses: + BadRequest: + description: Bad request - invalid parameters or malformed request + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://api.company.com/problems/bad-request + title: Bad Request + status: 400 + detail: "Invalid query parameter 'limit': must be between 1 and 100" + + NotFound: + description: Resource not found + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://api.company.com/problems/not-found + title: Not Found + status: 404 + detail: {{resource_title}} with the specified ID was not found + + Conflict: + description: Conflict - resource already exists or state conflict + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://api.company.com/problems/conflict + title: Conflict + status: 409 + detail: {{resource_title}} with this identifier already exists + + InternalError: + description: Internal server error + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + example: + type: https://api.company.com/problems/internal-error + title: Internal Server Error + status: 500 + detail: An unexpected error occurred while processing the request + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT-based authentication + +security: + - bearerAuth: [] diff --git a/skills/api.generatemodels/SKILL.md b/skills/api.generatemodels/SKILL.md new file mode 100644 index 0000000..948e3c9 --- /dev/null +++ b/skills/api.generatemodels/SKILL.md @@ -0,0 +1,299 @@ +# api.generate-models + +## Overview + +**api.generate-models** generates type-safe models from OpenAPI and AsyncAPI specifications, enabling shared models between frontend and backend using code generation. + +## Purpose + +Transform API specifications into type-safe code: +- Generate TypeScript interfaces from OpenAPI schemas +- Generate Python dataclasses/Pydantic models +- Generate Java classes, Go structs, C# classes +- Single source of truth: the API specification +- Automatic synchronization when specs change + +## Usage + +### Basic Usage + +```bash +python skills/api.generate-models/modelina_generate.py [options] +``` + +### Parameters + +| Parameter | Required | Description | Default | +|-----------|----------|-------------|---------| +| `spec_path` | Yes | Path to API spec file | - | +| `language` | Yes | Target language | - | +| `--output-dir` | No | Output directory | `src/models` | +| `--package-name` | No | Package/module name | - | + +### Supported Languages + +| Language | Extension | Status | +|----------|-----------|--------| +| `typescript` | `.ts` | ✅ Supported | +| `python` | `.py` | ✅ Supported | +| `java` | `.java` | 🚧 Planned | +| `go` | `.go` | 🚧 Planned | +| `csharp` | `.cs` | 🚧 Planned | +| `rust` | `.rs` | 🚧 Planned | + +## Examples + +### Example 1: Generate TypeScript Models + +```bash +python skills/api.generate-models/modelina_generate.py \ + specs/user-service.openapi.yaml \ + typescript \ + --output-dir=src/models/user-service +``` + +**Generated files**: +``` +src/models/user-service/ +├── User.ts +├── UserCreate.ts +├── UserUpdate.ts +├── Pagination.ts +└── Problem.ts +``` + +**Example TypeScript output**: +```typescript +// src/models/user-service/User.ts +export interface User { + /** Unique identifier */ + user_id: string; + /** Creation timestamp */ + created_at: string; + /** Last update timestamp */ + updated_at?: string; +} + +// src/models/user-service/Pagination.ts +export interface Pagination { + /** Number of items per page */ + limit: number; + /** Number of items skipped */ + offset: number; + /** Total number of items available */ + total: number; +} +``` + +### Example 2: Generate Python Models + +```bash +python skills/api.generate-models/modelina_generate.py \ + specs/user-service.openapi.yaml \ + python \ + --output-dir=src/models/user_service +``` + +**Generated files**: +``` +src/models/user_service/ +└── models.py +``` + +**Example Python output**: +```python +# src/models/user_service/models.py +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from uuid import UUID + +class User(BaseModel): + """User model""" + user_id: UUID = Field(..., description="Unique identifier") + created_at: datetime = Field(..., description="Creation timestamp") + updated_at: Optional[datetime] = Field(None, description="Last update timestamp") + +class Pagination(BaseModel): + """Pagination metadata""" + limit: int = Field(..., description="Number of items per page") + offset: int = Field(..., description="Number of items skipped") + total: int = Field(..., description="Total number of items available") +``` + +### Example 3: Generate for Multiple Languages + +```bash +# TypeScript for frontend +python skills/api.generate-models/modelina_generate.py \ + specs/user-service.openapi.yaml \ + typescript \ + --output-dir=frontend/src/models + +# Python for backend +python skills/api.generate-models/modelina_generate.py \ + specs/user-service.openapi.yaml \ + python \ + --output-dir=backend/app/models +``` + +## Code Generators Used + +The skill uses multiple code generation approaches: + +### 1. datamodel-code-generator (Primary) + +**Best for**: OpenAPI specs → Python/TypeScript +**Installation**: `pip install datamodel-code-generator` + +Generates: +- Python: Pydantic v2 models with type hints +- TypeScript: Type-safe interfaces +- Validates schema during generation + +### 2. Simple Built-in Generator (Fallback) + +**Best for**: Basic models when external tools not available +**Installation**: None required + +Generates: +- Python: dataclasses +- TypeScript: interfaces +- Basic but reliable + +### 3. Modelina (Future) + +**Best for**: AsyncAPI specs, multiple languages +**Installation**: `npm install -g @asyncapi/modelina` +**Status**: Planned + +## Output + +### Success Response + +```json +{ + "status": "success", + "data": { + "models_path": "src/models/user-service", + "files_generated": [ + "src/models/user-service/User.ts", + "src/models/user-service/UserCreate.ts", + "src/models/user-service/Pagination.ts", + "src/models/user-service/Problem.ts" + ], + "model_count": 4, + "generator_used": "datamodel-code-generator" + } +} +``` + +## Integration with Workflows + +```yaml +# workflows/api_first_development.yaml +steps: + - skill: api.define + args: + - "user-service" + - "openapi" + output: spec_path + + - skill: api.validate + args: + - "{spec_path}" + - "zalando" + required: true + + - skill: api.generate-models + args: + - "{spec_path}" + - "typescript" + - "--output-dir=frontend/src/models" + + - skill: api.generate-models + args: + - "{spec_path}" + - "python" + - "--output-dir=backend/app/models" +``` + +## Integration with Hooks + +Auto-regenerate models when specs change: + +```bash +python skills/hook.define/hook_define.py \ + on_file_save \ + "python betty/skills/api.generate-models/modelina_generate.py {file_path} typescript --output-dir=src/models" \ + --pattern="specs/*.openapi.yaml" \ + --blocking=false \ + --description="Auto-regenerate TypeScript models when OpenAPI specs change" +``` + +## Benefits + +### For Developers +- ✅ **Type safety**: Catch errors at compile time, not runtime +- ✅ **IDE autocomplete**: Full IntelliSense/autocomplete support +- ✅ **No manual typing**: Models generated automatically +- ✅ **Always in sync**: Regenerate when spec changes + +### For Teams +- ✅ **Single source of truth**: API spec defines types +- ✅ **Frontend/backend alignment**: Same types everywhere +- ✅ **Reduced errors**: Type mismatches caught early +- ✅ **Faster development**: No manual model creation + +### For Organizations +- ✅ **Consistency**: All services use same model generation +- ✅ **Maintainability**: Update spec → regenerate → done +- ✅ **Documentation**: Types are self-documenting +- ✅ **Quality**: Generated code is tested and reliable + +## Dependencies + +### Required +- **PyYAML**: For YAML parsing (`pip install pyyaml`) + +### Optional (Better Output) +- **datamodel-code-generator**: For high-quality Python/TypeScript (`pip install datamodel-code-generator`) +- **Node.js + Modelina**: For AsyncAPI and more languages (`npm install -g @asyncapi/modelina`) + +## Examples with Real Specs + +Using the user-service spec from Phase 1: + +```bash +# Generate TypeScript +python skills/api.generate-models/modelina_generate.py \ + specs/user-service.openapi.yaml \ + typescript + +# Output: +{ + "status": "success", + "data": { + "models_path": "src/models", + "files_generated": [ + "src/models/User.ts", + "src/models/UserCreate.ts", + "src/models/UserUpdate.ts", + "src/models/Pagination.ts", + "src/models/Problem.ts" + ], + "model_count": 5 + } +} +``` + +## See Also + +- [api.define](../api.define/SKILL.md) - Create OpenAPI specs +- [api.validate](../api.validate/SKILL.md) - Validate specs +- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model +- [API-Driven Development](../../docs/api-driven-development.md) - Complete guide + +## Version + +**0.1.0** - Initial implementation with TypeScript and Python support diff --git a/skills/api.generatemodels/__init__.py b/skills/api.generatemodels/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/api.generatemodels/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/api.generatemodels/modelina_generate.py b/skills/api.generatemodels/modelina_generate.py new file mode 100755 index 0000000..c62e991 --- /dev/null +++ b/skills/api.generatemodels/modelina_generate.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +""" +Generate type-safe models from OpenAPI and AsyncAPI specifications using Modelina. + +This skill uses AsyncAPI Modelina to generate models in various languages +from API specifications. +""" + +import sys +import json +import argparse +import subprocess +import shutil +from pathlib import Path +from typing import Dict, Any, List + +# Add betty module to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response, BettyError +from betty.validation import validate_path + +logger = setup_logger(__name__) + +# Supported languages +SUPPORTED_LANGUAGES = [ + "typescript", + "python", + "java", + "go", + "csharp", + "rust", + "kotlin", + "dart" +] + +# Language-specific configurations +LANGUAGE_CONFIG = { + "typescript": { + "extension": ".ts", + "package_json_required": False, + "modelina_generator": "typescript" + }, + "python": { + "extension": ".py", + "package_json_required": False, + "modelina_generator": "python" + }, + "java": { + "extension": ".java", + "package_json_required": False, + "modelina_generator": "java" + }, + "go": { + "extension": ".go", + "package_json_required": False, + "modelina_generator": "go" + }, + "csharp": { + "extension": ".cs", + "package_json_required": False, + "modelina_generator": "csharp" + } +} + + +def check_node_installed() -> bool: + """ + Check if Node.js is installed. + + Returns: + True if Node.js is available, False otherwise + """ + try: + result = subprocess.run( + ["node", "--version"], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode == 0: + version = result.stdout.strip() + logger.info(f"Node.js found: {version}") + return True + return False + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def check_npx_installed() -> bool: + """ + Check if npx is installed. + + Returns: + True if npx is available, False otherwise + """ + try: + result = subprocess.run( + ["npx", "--version"], + capture_output=True, + text=True, + timeout=5 + ) + return result.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def generate_modelina_script( + spec_path: str, + language: str, + output_dir: str, + package_name: str = None +) -> str: + """ + Generate a Node.js script that uses Modelina to generate models. + + Args: + spec_path: Path to spec file + language: Target language + output_dir: Output directory + package_name: Package name (optional) + + Returns: + JavaScript code as string + """ + # Modelina generator based on language + generator_map = { + "typescript": "TypeScriptGenerator", + "python": "PythonGenerator", + "java": "JavaGenerator", + "go": "GoGenerator", + "csharp": "CSharpGenerator" + } + + generator_class = generator_map.get(language, "TypeScriptGenerator") + + script = f""" +const {{ {generator_class} }} = require('@asyncapi/modelina'); +const fs = require('fs'); +const path = require('path'); + +async function generate() {{ + try {{ + // Read the spec file + const spec = fs.readFileSync('{spec_path}', 'utf8'); + const specData = JSON.parse(spec); + + // Create generator + const generator = new {generator_class}(); + + // Generate models + const models = await generator.generate(specData); + + // Ensure output directory exists + const outputDir = '{output_dir}'; + if (!fs.existsSync(outputDir)) {{ + fs.mkdirSync(outputDir, {{ recursive: true }}); + }} + + // Write models to files + const filesGenerated = []; + for (const model of models) {{ + const filePath = path.join(outputDir, model.name + model.extension); + fs.writeFileSync(filePath, model.result); + filesGenerated.push(filePath); + }} + + // Output result + console.log(JSON.stringify({{ + success: true, + files_generated: filesGenerated, + model_count: models.length + }})); + + }} catch (error) {{ + console.error(JSON.stringify({{ + success: false, + error: error.message, + stack: error.stack + }})); + process.exit(1); + }} +}} + +generate(); +""" + return script + + +def generate_models_datamodel_code_generator( + spec_path: str, + language: str, + output_dir: str, + package_name: str = None +) -> Dict[str, Any]: + """ + Generate models using datamodel-code-generator (Python fallback). + + This is used when Modelina/Node.js is not available. + Works for OpenAPI specs only, generating Python/TypeScript models. + + Args: + spec_path: Path to specification file + language: Target language + output_dir: Output directory + package_name: Package name + + Returns: + Result dictionary + """ + try: + # Check if datamodel-code-generator is installed + result = subprocess.run( + ["datamodel-codegen", "--version"], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode != 0: + raise BettyError( + "datamodel-code-generator not installed. " + "Install with: pip install datamodel-code-generator" + ) + + except FileNotFoundError: + raise BettyError( + "datamodel-code-generator not found. " + "Install with: pip install datamodel-code-generator" + ) + + # Create output directory + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + # Determine output file based on language + if language == "python": + output_file = output_path / "models.py" + cmd = [ + "datamodel-codegen", + "--input", spec_path, + "--output", str(output_file), + "--input-file-type", "openapi", + "--output-model-type", "pydantic_v2.BaseModel", + "--snake-case-field", + "--use-standard-collections" + ] + elif language == "typescript": + output_file = output_path / "models.ts" + cmd = [ + "datamodel-codegen", + "--input", spec_path, + "--output", str(output_file), + "--input-file-type", "openapi", + "--output-model-type", "typescript" + ] + else: + raise BettyError( + f"datamodel-code-generator fallback only supports Python and TypeScript, not {language}" + ) + + # Run code generator + logger.info(f"Running datamodel-code-generator: {' '.join(cmd)}") + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=60 + ) + + if result.returncode != 0: + raise BettyError(f"Code generation failed: {result.stderr}") + + # Count generated files + files_generated = [str(output_file)] + + return { + "models_path": str(output_path), + "files_generated": files_generated, + "model_count": 1, + "generator_used": "datamodel-code-generator" + } + + +def generate_models_simple( + spec_path: str, + language: str, + output_dir: str, + package_name: str = None +) -> Dict[str, Any]: + """ + Simple model generation without external tools. + + Generates basic model files from OpenAPI schemas as a last resort. + + Args: + spec_path: Path to specification file + language: Target language + output_dir: Output directory + package_name: Package name + + Returns: + Result dictionary + """ + import yaml + + # Load spec + with open(spec_path, 'r') as f: + spec = yaml.safe_load(f) + + # Get schemas + schemas = spec.get("components", {}).get("schemas", {}) + + if not schemas: + raise BettyError("No schemas found in specification") + + # Create output directory + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + files_generated = [] + + # Generate basic models for each schema + for schema_name, schema_def in schemas.items(): + if language == "typescript": + content = generate_typescript_interface(schema_name, schema_def) + file_path = output_path / f"{schema_name}.ts" + elif language == "python": + content = generate_python_dataclass(schema_name, schema_def) + file_path = output_path / f"{schema_name.lower()}.py" + else: + raise BettyError(f"Simple generation only supports TypeScript and Python, not {language}") + + with open(file_path, 'w') as f: + f.write(content) + + files_generated.append(str(file_path)) + logger.info(f"Generated {file_path}") + + return { + "models_path": str(output_path), + "files_generated": files_generated, + "model_count": len(schemas), + "generator_used": "simple" + } + + +def generate_typescript_interface(name: str, schema: Dict[str, Any]) -> str: + """Generate TypeScript interface from schema.""" + properties = schema.get("properties") or {} + required = schema.get("required", []) + + lines = [f"export interface {name} {{"] + + if not properties: + lines.append(" // No properties defined") + + for prop_name, prop_def in properties.items(): + prop_type = map_openapi_type_to_typescript(prop_def.get("type", "any")) + optional = "" if prop_name in required else "?" + description = prop_def.get("description", "") + + if description: + lines.append(f" /** {description} */") + lines.append(f" {prop_name}{optional}: {prop_type};") + + lines.append("}") + + return "\n".join(lines) + + +def generate_python_dataclass(name: str, schema: Dict[str, Any]) -> str: + """Generate Python dataclass from schema.""" + properties = schema.get("properties") or {} + required = schema.get("required", []) + + lines = [ + "from dataclasses import dataclass", + "from typing import Optional", + "from datetime import datetime", + "", + "@dataclass", + f"class {name}:" + ] + + if not properties: + lines.append(" pass") + else: + for prop_name, prop_def in properties.items(): + prop_type = map_openapi_type_to_python(prop_def) + description = prop_def.get("description", "") + + if prop_name not in required: + prop_type = f"Optional[{prop_type}]" + + if description: + lines.append(f" # {description}") + + default = " = None" if prop_name not in required else "" + lines.append(f" {prop_name}: {prop_type}{default}") + + return "\n".join(lines) + + +def map_openapi_type_to_typescript(openapi_type: str) -> str: + """Map OpenAPI type to TypeScript type.""" + type_map = { + "string": "string", + "number": "number", + "integer": "number", + "boolean": "boolean", + "array": "any[]", + "object": "object" + } + return type_map.get(openapi_type, "any") + + +def map_openapi_type_to_python(prop_def: Dict[str, Any]) -> str: + """Map OpenAPI type to Python type.""" + openapi_type = prop_def.get("type", "Any") + format_type = prop_def.get("format", "") + + if openapi_type == "string": + if format_type == "date-time": + return "datetime" + elif format_type == "uuid": + return "str" # or UUID from uuid module + return "str" + elif openapi_type == "number" or openapi_type == "integer": + return "int" if openapi_type == "integer" else "float" + elif openapi_type == "boolean": + return "bool" + elif openapi_type == "array": + return "list" + elif openapi_type == "object": + return "dict" + return "Any" + + +def generate_models( + spec_path: str, + language: str, + output_dir: str = "src/models", + package_name: str = None +) -> Dict[str, Any]: + """ + Generate models from API specification. + + Args: + spec_path: Path to specification file + language: Target language + output_dir: Output directory + package_name: Package name + + Returns: + Result dictionary with generated files info + + Raises: + BettyError: If generation fails + """ + # Validate language + if language not in SUPPORTED_LANGUAGES: + raise BettyError( + f"Unsupported language '{language}'. " + f"Supported: {', '.join(SUPPORTED_LANGUAGES)}" + ) + + # Validate spec file exists + if not Path(spec_path).exists(): + raise BettyError(f"Specification file not found: {spec_path}") + + logger.info(f"Generating {language} models from {spec_path}") + + # Try datamodel-code-generator first (most reliable for OpenAPI) + try: + logger.info("Attempting generation with datamodel-code-generator") + result = generate_models_datamodel_code_generator( + spec_path, language, output_dir, package_name + ) + return result + except BettyError as e: + logger.warning(f"datamodel-code-generator not available: {e}") + + # Fallback to simple generation + logger.info("Using simple built-in generator") + result = generate_models_simple( + spec_path, language, output_dir, package_name + ) + + return result + + +def main(): + parser = argparse.ArgumentParser( + description="Generate type-safe models from API specifications using Modelina" + ) + parser.add_argument( + "spec_path", + type=str, + help="Path to API specification file (OpenAPI or AsyncAPI)" + ) + parser.add_argument( + "language", + type=str, + choices=SUPPORTED_LANGUAGES, + help="Target language for generated models" + ) + parser.add_argument( + "--output-dir", + type=str, + default="src/models", + help="Output directory for generated models (default: src/models)" + ) + parser.add_argument( + "--package-name", + type=str, + help="Package/module name for generated code" + ) + + args = parser.parse_args() + + try: + # Validate inputs + validate_path(args.spec_path) + + # Generate models + result = generate_models( + spec_path=args.spec_path, + language=args.language, + output_dir=args.output_dir, + package_name=args.package_name + ) + + # Return structured result + output = { + "status": "success", + "data": result + } + print(json.dumps(output, indent=2)) + + except Exception as e: + logger.error(f"Model generation failed: {e}") + print(json.dumps(format_error_response(e), indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/api.generatemodels/skill.yaml b/skills/api.generatemodels/skill.yaml new file mode 100644 index 0000000..f21fa5a --- /dev/null +++ b/skills/api.generatemodels/skill.yaml @@ -0,0 +1,53 @@ +name: api.generatemodels +version: 0.1.0 +description: Generate type-safe models from OpenAPI and AsyncAPI specifications using Modelina + +inputs: + - name: spec_path + type: string + required: true + description: Path to API specification file (OpenAPI or AsyncAPI) + + - name: language + type: string + required: true + description: Target language (typescript, python, java, go, csharp) + + - name: output_dir + type: string + required: false + default: src/models + description: Output directory for generated models + + - name: package_name + type: string + required: false + description: Package/module name for generated code + +outputs: + - name: models_path + type: string + description: Path to directory containing generated models + + - name: files_generated + type: array + description: List of generated model files + + - name: model_count + type: number + description: Number of models generated + +dependencies: + - context.schema + +entrypoints: + - command: /skill/api/generate-models + handler: modelina_generate.py + runtime: python + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: [api, codegen, modelina, openapi, asyncapi, typescript, python, java] diff --git a/skills/api.test/README.md b/skills/api.test/README.md new file mode 100644 index 0000000..c36ee2c --- /dev/null +++ b/skills/api.test/README.md @@ -0,0 +1,83 @@ +# api.test + +Test REST API endpoints by executing HTTP requests and validating responses against expected outcomes + +## Overview + +**Purpose:** Test REST API endpoints by executing HTTP requests and validating responses against expected outcomes + +**Command:** `/api/test` + +## Usage + +### Basic Usage + +```bash +python3 skills/api/test/api_test.py +``` + +### With Arguments + +```bash +python3 skills/api/test/api_test.py \ + --api_spec_path "value" \ + --base_url "value" \ + --test_scenarios_path_(optional) "value" \ + --auth_config_path_(optional) "value" \ + --output-format json +``` + +## Inputs + +- **api_spec_path** +- **base_url** +- **test_scenarios_path (optional)** +- **auth_config_path (optional)** + +## Outputs + +- **test_results.json** +- **test_report.html** + +## Artifact Metadata + +### Produces + +- `test-result` +- `test-report` + +## Permissions + +- `network:http` +- `filesystem:read` +- `filesystem:write` + +## Implementation Notes + +Support multiple HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS Test scenarios should validate: - Response status codes - Response headers - Response body structure and content - Response time/performance - Authentication/authorization - Error handling Features: - Load test scenarios from OpenAPI/Swagger specs - Support various authentication methods (Bearer, Basic, API Key, OAuth2) - Execute tests in sequence or parallel - Generate detailed HTML reports with pass/fail visualization - Support environment variables for configuration - Retry failed tests with exponential backoff - Collect performance metrics (response time, throughput) Output should include: - Total tests run - Passed/failed counts - Individual test results with request/response details - Performance statistics - Coverage metrics (% of endpoints tested) + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - api.test +``` + +## Testing + +Run tests with: + +```bash +pytest skills/api/test/test_api_test.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/api.test/__init__.py b/skills/api.test/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/api.test/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/api.test/api_test.py b/skills/api.test/api_test.py new file mode 100755 index 0000000..3c76285 --- /dev/null +++ b/skills/api.test/api_test.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +api.test - Test REST API endpoints by executing HTTP requests and validating responses against expected outcomes + +Generated by meta.skill +""" + +import os +import sys +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +class ApiTest: + """ + Test REST API endpoints by executing HTTP requests and validating responses against expected outcomes + """ + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + def execute(self, api_spec_path: Optional[str] = None, base_url: Optional[str] = None, test_scenarios_path_optional: Optional[str] = None, auth_config_path_optional: Optional[str] = None) -> Dict[str, Any]: + """ + Execute the skill + + Returns: + Dict with execution results + """ + try: + logger.info("Executing api.test...") + + # TODO: Implement skill logic here + + # Implementation notes: + # Support multiple HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS Test scenarios should validate: - Response status codes - Response headers - Response body structure and content - Response time/performance - Authentication/authorization - Error handling Features: - Load test scenarios from OpenAPI/Swagger specs - Support various authentication methods (Bearer, Basic, API Key, OAuth2) - Execute tests in sequence or parallel - Generate detailed HTML reports with pass/fail visualization - Support environment variables for configuration - Retry failed tests with exponential backoff - Collect performance metrics (response time, throughput) Output should include: - Total tests run - Passed/failed counts - Individual test results with request/response details - Performance statistics - Coverage metrics (% of endpoints tested) + + # Placeholder implementation + result = { + "ok": True, + "status": "success", + "message": "Skill executed successfully" + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Test REST API endpoints by executing HTTP requests and validating responses against expected outcomes" + ) + + parser.add_argument( + "--api-spec-path", + help="api_spec_path" + ) + parser.add_argument( + "--base-url", + help="base_url" + ) + parser.add_argument( + "--test-scenarios-path-optional", + help="test_scenarios_path (optional)" + ) + parser.add_argument( + "--auth-config-path-optional", + help="auth_config_path (optional)" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = ApiTest() + + # Execute skill + result = skill.execute( + api_spec_path=args.api_spec_path, + base_url=args.base_url, + test_scenarios_path_optional=args.test_scenarios_path_optional, + auth_config_path_optional=args.auth_config_path_optional, + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/api.test/skill.yaml b/skills/api.test/skill.yaml new file mode 100644 index 0000000..7e29ec8 --- /dev/null +++ b/skills/api.test/skill.yaml @@ -0,0 +1,27 @@ +name: api.test +version: 0.1.0 +description: Test REST API endpoints by executing HTTP requests and validating responses + against expected outcomes +inputs: +- api_spec_path +- base_url +- test_scenarios_path (optional) +- auth_config_path (optional) +outputs: +- test_results.json +- test_report.html +status: active +permissions: +- network:http +- filesystem:read +- filesystem:write +entrypoints: +- command: /api/test + handler: api_test.py + runtime: python + description: Test REST API endpoints by executing HTTP requests and validating responses + against expected outcome +artifact_metadata: + produces: + - type: test-result + - type: test-report diff --git a/skills/api.test/test_api_test.py b/skills/api.test/test_api_test.py new file mode 100644 index 0000000..8486c0a --- /dev/null +++ b/skills/api.test/test_api_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for api.test + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.api_test import api_test + + +class TestApiTest: + """Tests for ApiTest""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = api_test.ApiTest() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["api_test.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + api_test.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Test REST API endpoints by executing HTTP requests" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/api.validate/SKILL.md b/skills/api.validate/SKILL.md new file mode 100644 index 0000000..478ffd8 --- /dev/null +++ b/skills/api.validate/SKILL.md @@ -0,0 +1,315 @@ +# api.validate + +## Overview + +**api.validate** validates OpenAPI and AsyncAPI specifications against enterprise guidelines, with built-in support for Zalando RESTful API Guidelines. + +## Purpose + +Ensure API specifications meet enterprise standards: +- Validate OpenAPI 3.x specifications +- Validate AsyncAPI 3.x specifications +- Check compliance with Zalando guidelines +- Detect common API design mistakes +- Provide actionable suggestions for fixes + +## Usage + +### Basic Usage + +```bash +python skills/api.validate/api_validate.py [guideline_set] [options] +``` + +### Parameters + +| Parameter | Required | Description | Default | +|-----------|----------|-------------|---------| +| `spec_path` | Yes | Path to API spec file | - | +| `guideline_set` | No | Guidelines to validate against | `zalando` | +| `--strict` | No | Warnings become errors | `false` | +| `--format` | No | Output format (json, human) | `json` | + +### Guideline Sets + +| Guideline | Status | Description | +|-----------|--------|-------------| +| `zalando` | ✅ Supported | Zalando RESTful API Guidelines | +| `google` | 🚧 Planned | Google API Design Guide | +| `microsoft` | 🚧 Planned | Microsoft REST API Guidelines | + +## Examples + +### Example 1: Validate OpenAPI Spec + +```bash +python skills/api.validate/api_validate.py specs/user-service.openapi.yaml zalando +``` + +**Output** (JSON format): +```json +{ + "status": "success", + "data": { + "valid": false, + "errors": [ + { + "rule_id": "MUST_001", + "message": "Missing required field 'info.x-api-id'", + "severity": "error", + "path": "info.x-api-id", + "suggestion": "Add a UUID to uniquely identify this API" + } + ], + "warnings": [ + { + "rule_id": "SHOULD_001", + "message": "Missing 'info.contact'", + "severity": "warning", + "path": "info.contact" + } + ], + "spec_path": "specs/user-service.openapi.yaml", + "spec_type": "openapi", + "guideline_set": "zalando" + } +} +``` + +### Example 2: Human-Readable Output + +```bash +python skills/api.validate/api_validate.py \ + specs/user-service.openapi.yaml \ + zalando \ + --format=human +``` + +**Output**: +``` +============================================================ +API Validation Report +============================================================ +Spec: specs/user-service.openapi.yaml +Type: OPENAPI +Guidelines: zalando +============================================================ + +❌ ERRORS (1): + [MUST_001] Missing required field 'info.x-api-id' + Path: info.x-api-id + 💡 Add a UUID to uniquely identify this API + +⚠️ WARNINGS (1): + [SHOULD_001] Missing 'info.contact' + Path: info.contact + 💡 Add contact information + +============================================================ +❌ Validation FAILED +============================================================ +``` + +### Example 3: Strict Mode + +```bash +python skills/api.validate/api_validate.py \ + specs/user-service.openapi.yaml \ + zalando \ + --strict +``` + +In strict mode, warnings are treated as errors. Use for CI/CD pipelines where you want zero tolerance for issues. + +### Example 4: Validate AsyncAPI Spec + +```bash +python skills/api.validate/api_validate.py specs/user-events.asyncapi.yaml +``` + +## Validation Rules + +### Zalando Guidelines (OpenAPI) + +#### MUST Rules (Errors) + +| Rule | Description | Example Fix | +|------|-------------|-------------| +| **MUST_001** | Required `x-api-id` metadata | `x-api-id: 'd0184f38-b98d-11e7-9c56-68f728c1ba70'` | +| **MUST_002** | Required `x-audience` metadata | `x-audience: 'company-internal'` | +| **MUST_003** | Path naming conventions | Use lowercase kebab-case or snake_case | +| **MUST_004** | Property naming (snake_case) | `userId` → `user_id` | +| **MUST_005** | HTTP method usage | GET should not have requestBody | + +#### SHOULD Rules (Warnings) + +| Rule | Description | Example Fix | +|------|-------------|-------------| +| **SHOULD_001** | Contact information | Add `info.contact` with team details | +| **SHOULD_002** | POST returns 201 | Add 201 response to POST operations | +| **SHOULD_003** | Document 400 errors | Add 400 Bad Request response | +| **SHOULD_004** | Document 500 errors | Add 500 Internal Error response | +| **SHOULD_005** | 201 includes Location header | Add Location header to 201 responses | +| **SHOULD_006** | Problem schema for errors | Define RFC 7807 Problem schema | +| **SHOULD_007** | Error responses use application/problem+json | Use correct content type | +| **SHOULD_008** | X-Flow-ID header | Add request tracing header | +| **SHOULD_009** | Security schemes defined | Add authentication schemes | + +### AsyncAPI Guidelines + +| Rule | Description | +|------|-------------| +| **ASYNCAPI_001** | Required `info` field | +| **ASYNCAPI_002** | Required `channels` field | +| **ASYNCAPI_003** | Version check (recommend 3.x) | + +## Integration with Hooks + +### Automatic Validation on File Edit + +```bash +# Create hook using hook.define +python skills/hook.define/hook_define.py \ + on_file_edit \ + "python betty/skills/api.validate/api_validate.py {file_path} zalando" \ + --pattern="*.openapi.yaml" \ + --blocking=true \ + --timeout=10000 \ + --description="Validate OpenAPI specs on edit" +``` + +**Result**: Every time you edit a `*.openapi.yaml` file, it's automatically validated. If validation fails, the edit is blocked. + +### Validation on Commit + +```bash +python skills/hook.define/hook_define.py \ + on_commit \ + "python betty/skills/api.validate/api_validate.py {file_path} zalando --strict" \ + --pattern="specs/**/*.yaml" \ + --blocking=true \ + --description="Prevent commits with invalid specs" +``` + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Validation passed (no errors) | +| `1` | Validation failed (has errors) or execution error | + +## Common Validation Errors + +### Missing x-api-id + +**Error**: +``` +Missing required field 'info.x-api-id' +``` + +**Fix**: +```yaml +info: + title: My API + version: 1.0.0 + x-api-id: d0184f38-b98d-11e7-9c56-68f728c1ba70 # Add this +``` + +### Wrong Property Naming + +**Error**: +``` +Property 'userId' should use snake_case +``` + +**Fix**: +```yaml +# Before +properties: + userId: + type: string + +# After +properties: + user_id: + type: string +``` + +### Missing Error Responses + +**Error**: +``` +GET operation should document 400 (Bad Request) response +``` + +**Fix**: +```yaml +responses: + '200': + description: Success + '400': # Add this + $ref: '#/components/responses/BadRequest' + '500': # Add this + $ref: '#/components/responses/InternalError' +``` + +### Wrong Content Type for Errors + +**Error**: +``` +Error response 400 should use 'application/problem+json' +``` + +**Fix**: +```yaml +'400': + description: Bad request + content: + application/problem+json: # Not application/json + schema: + $ref: '#/components/schemas/Problem' +``` + +## Use in Workflows + +```yaml +# workflows/api_validation_suite.yaml +steps: + - skill: api.validate + args: + - "specs/user-service.openapi.yaml" + - "zalando" + - "--strict" + required: true +``` + +## Dependencies + +- **PyYAML**: Required for YAML parsing + ```bash + pip install pyyaml + ``` + +## Files + +### Input +- `*.openapi.yaml` - OpenAPI 3.x specifications +- `*.asyncapi.yaml` - AsyncAPI 3.x specifications +- `*.json` - JSON format specifications + +### Output +- JSON validation report (stdout) +- Human-readable report (with `--format=human`) + +## See Also + +- [hook.define](../hook.define/SKILL.md) - Create validation hooks +- [api.define](../api.define/SKILL.md) - Create OpenAPI specs +- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model +- [API-Driven Development](../../docs/api-driven-development.md) - Complete guide +- [Zalando API Guidelines](https://opensource.zalando.com/restful-api-guidelines/) +- [RFC 7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807) + +## Version + +**0.1.0** - Initial implementation with Zalando guidelines support diff --git a/skills/api.validate/__init__.py b/skills/api.validate/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/api.validate/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/api.validate/api_validate.py b/skills/api.validate/api_validate.py new file mode 100755 index 0000000..5f9ef85 --- /dev/null +++ b/skills/api.validate/api_validate.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +""" +Validate OpenAPI and AsyncAPI specifications against enterprise guidelines. + +Supports: +- OpenAPI 3.x specifications +- AsyncAPI 3.x specifications +- Zalando RESTful API Guidelines +- Custom enterprise guidelines +""" + +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, Any + +# Add betty module to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response, BettyError +from betty.validation import validate_path +from betty.telemetry_capture import telemetry_decorator +from .validators.zalando_rules import ZalandoValidator + +logger = setup_logger(__name__) + + +def load_spec(spec_path: str) -> Dict[str, Any]: + """ + Load API specification from file. + + Args: + spec_path: Path to YAML or JSON specification file + + Returns: + Parsed specification dictionary + + Raises: + BettyError: If file cannot be loaded or parsed + """ + spec_file = Path(spec_path) + + if not spec_file.exists(): + raise BettyError(f"Specification file not found: {spec_path}") + + if not spec_file.is_file(): + raise BettyError(f"Path is not a file: {spec_path}") + + try: + import yaml + with open(spec_file, 'r') as f: + spec = yaml.safe_load(f) + + if not isinstance(spec, dict): + raise BettyError("Specification must be a valid YAML/JSON object") + + logger.info(f"Loaded specification from {spec_path}") + return spec + + except yaml.YAMLError as e: + raise BettyError(f"Failed to parse YAML: {e}") + except Exception as e: + raise BettyError(f"Failed to load specification: {e}") + + +def detect_spec_type(spec: Dict[str, Any]) -> str: + """ + Detect specification type (OpenAPI or AsyncAPI). + + Args: + spec: Parsed specification + + Returns: + Specification type: "openapi" or "asyncapi" + + Raises: + BettyError: If type cannot be determined + """ + if "openapi" in spec: + version = spec["openapi"] + logger.info(f"Detected OpenAPI {version} specification") + return "openapi" + elif "asyncapi" in spec: + version = spec["asyncapi"] + logger.info(f"Detected AsyncAPI {version} specification") + return "asyncapi" + else: + raise BettyError( + "Could not detect specification type. Must contain 'openapi' or 'asyncapi' field." + ) + + +def validate_openapi_zalando(spec: Dict[str, Any], strict: bool = False) -> Dict[str, Any]: + """Validate an OpenAPI specification against Zalando guidelines.""" + validator = ZalandoValidator(spec, strict=strict) + report = validator.validate() + + logger.info( + f"Validation complete: {len(report['errors'])} errors, " + f"{len(report['warnings'])} warnings" + ) + + return report + + +def validate_asyncapi(spec: Dict[str, Any], strict: bool = False) -> Dict[str, Any]: + """ + Validate AsyncAPI specification. + + Args: + spec: AsyncAPI specification + strict: Enable strict mode + + Returns: + Validation report + """ + # Basic AsyncAPI validation + errors = [] + warnings = [] + + # Check required fields + if "info" not in spec: + errors.append({ + "rule_id": "ASYNCAPI_001", + "message": "Missing required field 'info'", + "severity": "error", + "path": "info" + }) + + if "channels" not in spec: + errors.append({ + "rule_id": "ASYNCAPI_002", + "message": "Missing required field 'channels'", + "severity": "error", + "path": "channels" + }) + + # Check version + asyncapi_version = spec.get("asyncapi", "unknown") + if not asyncapi_version.startswith("3."): + warnings.append({ + "rule_id": "ASYNCAPI_003", + "message": f"AsyncAPI version {asyncapi_version} - consider upgrading to 3.x", + "severity": "warning", + "path": "asyncapi" + }) + + logger.info(f"AsyncAPI validation complete: {len(errors)} errors, {len(warnings)} warnings") + + return { + "valid": len(errors) == 0, + "errors": errors, + "warnings": warnings, + "spec_version": asyncapi_version, + "rules_checked": [ + "ASYNCAPI_001: Required info field", + "ASYNCAPI_002: Required channels field", + "ASYNCAPI_003: Version check" + ] + } + + +def validate_spec( + spec_path: str, + guideline_set: str = "zalando", + strict: bool = False +) -> Dict[str, Any]: + """ + Validate API specification against guidelines. + + Args: + spec_path: Path to specification file + guideline_set: Guidelines to validate against + strict: Enable strict mode + + Returns: + Validation report + + Raises: + BettyError: If validation fails + """ + # Load specification + spec = load_spec(spec_path) + + # Detect type + spec_type = detect_spec_type(spec) + + # Validate based on type and guidelines + if spec_type == "openapi": + if guideline_set == "zalando": + report = validate_openapi_zalando(spec, strict=strict) + else: + raise BettyError( + f"Guideline set '{guideline_set}' not yet supported for OpenAPI. " + f"Supported: zalando" + ) + elif spec_type == "asyncapi": + report = validate_asyncapi(spec, strict=strict) + else: + raise BettyError(f"Unsupported specification type: {spec_type}") + + # Add metadata + report["spec_path"] = spec_path + report["spec_type"] = spec_type + report["guideline_set"] = guideline_set + + return report + + +def format_validation_output(report: Dict[str, Any]) -> str: + """ + Format validation report for human-readable output. + + Args: + report: Validation report + + Returns: + Formatted output string + """ + lines = [] + + # Header + spec_path = report.get("spec_path", "unknown") + spec_type = report.get("spec_type", "unknown").upper() + lines.append(f"\n{'='*60}") + lines.append(f"API Validation Report") + lines.append(f"{'='*60}") + lines.append(f"Spec: {spec_path}") + lines.append(f"Type: {spec_type}") + lines.append(f"Guidelines: {report.get('guideline_set', 'unknown')}") + lines.append(f"{'='*60}\n") + + # Errors + errors = report.get("errors", []) + if errors: + lines.append(f"❌ ERRORS ({len(errors)}):") + for error in errors: + lines.append(f" [{error.get('rule_id', 'UNKNOWN')}] {error.get('message', '')}") + if error.get('path'): + lines.append(f" Path: {error['path']}") + if error.get('suggestion'): + lines.append(f" 💡 {error['suggestion']}") + lines.append("") + + # Warnings + warnings = report.get("warnings", []) + if warnings: + lines.append(f"⚠️ WARNINGS ({len(warnings)}):") + for warning in warnings: + lines.append(f" [{warning.get('rule_id', 'UNKNOWN')}] {warning.get('message', '')}") + if warning.get('path'): + lines.append(f" Path: {warning['path']}") + if warning.get('suggestion'): + lines.append(f" 💡 {warning['suggestion']}") + lines.append("") + + # Summary + lines.append(f"{'='*60}") + if report.get("valid"): + lines.append("✅ Validation PASSED") + else: + lines.append("❌ Validation FAILED") + lines.append(f"{'='*60}\n") + + return "\n".join(lines) + + +@telemetry_decorator(skill_name="api.validate", caller="cli") +def main(): + parser = argparse.ArgumentParser( + description="Validate API specifications against enterprise guidelines" + ) + parser.add_argument( + "spec_path", + type=str, + help="Path to the API specification file (YAML or JSON)" + ) + parser.add_argument( + "guideline_set", + type=str, + nargs="?", + default="zalando", + choices=["zalando", "google", "microsoft"], + help="Guidelines to validate against (default: zalando)" + ) + parser.add_argument( + "--strict", + action="store_true", + help="Enable strict mode (warnings become errors)" + ) + parser.add_argument( + "--format", + type=str, + choices=["json", "human"], + default="json", + help="Output format (default: json)" + ) + + args = parser.parse_args() + + try: + # Check if PyYAML is installed + try: + import yaml + except ImportError: + raise BettyError( + "PyYAML is required for api.validate. Install with: pip install pyyaml" + ) + + # Validate inputs + validate_path(args.spec_path) + + # Run validation + logger.info(f"Validating {args.spec_path} against {args.guideline_set} guidelines") + report = validate_spec( + spec_path=args.spec_path, + guideline_set=args.guideline_set, + strict=args.strict + ) + + # Output based on format + if args.format == "human": + print(format_validation_output(report)) + else: + output = { + "status": "success", + "data": report + } + print(json.dumps(output, indent=2)) + + # Exit with error code if validation failed + if not report["valid"]: + sys.exit(1) + + except Exception as e: + logger.error(f"Validation failed: {e}") + print(json.dumps(format_error_response(e), indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/api.validate/skill.yaml b/skills/api.validate/skill.yaml new file mode 100644 index 0000000..e005daf --- /dev/null +++ b/skills/api.validate/skill.yaml @@ -0,0 +1,49 @@ +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 (OpenAPI or AsyncAPI) + + - name: guideline_set + type: string + required: false + default: zalando + description: Which API guidelines to validate against (zalando, google, microsoft) + + - name: strict + type: boolean + required: false + default: false + description: Enable strict mode (warnings become errors) + +outputs: + - name: validation_report + type: object + description: Detailed validation results including errors and warnings + + - name: valid + type: boolean + description: Whether the spec is valid + + - name: guideline_version + type: string + description: Version of guidelines used for validation + +dependencies: + - context.schema + +entrypoints: + - command: /skill/api/validate + handler: api_validate.py + runtime: python + permissions: + - filesystem:read + - network:http + +status: active + +tags: [api, validation, openapi, asyncapi, zalando] diff --git a/skills/api.validate/validators/__init__.py b/skills/api.validate/validators/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/api.validate/validators/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/api.validate/validators/zalando_rules.py b/skills/api.validate/validators/zalando_rules.py new file mode 100644 index 0000000..1ac9cb1 --- /dev/null +++ b/skills/api.validate/validators/zalando_rules.py @@ -0,0 +1,359 @@ +""" +Zalando RESTful API Guidelines validation rules. + +Based on: https://opensource.zalando.com/restful-api-guidelines/ +""" + +from typing import Dict, List, Any, Optional +import re + + +class ValidationError: + """Represents a validation error or warning.""" + + def __init__( + self, + rule_id: str, + message: str, + severity: str = "error", + path: Optional[str] = None, + suggestion: Optional[str] = None + ): + self.rule_id = rule_id + self.message = message + self.severity = severity # "error" or "warning" + self.path = path + self.suggestion = suggestion + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + result = { + "rule_id": self.rule_id, + "message": self.message, + "severity": self.severity + } + if self.path: + result["path"] = self.path + if self.suggestion: + result["suggestion"] = self.suggestion + return result + + +class ZalandoValidator: + """Validates OpenAPI specs against Zalando guidelines.""" + + def __init__(self, spec: Dict[str, Any], strict: bool = False): + self.spec = spec + self.strict = strict + self.errors: List[ValidationError] = [] + self.warnings: List[ValidationError] = [] + + def validate(self) -> Dict[str, Any]: + """ + Run all validation rules. + + Returns: + Validation report with errors and warnings + """ + # Required metadata + self._check_required_metadata() + + # Naming conventions + self._check_naming_conventions() + + # HTTP methods and status codes + self._check_http_methods() + self._check_status_codes() + + # Error handling + self._check_error_responses() + + # Headers + self._check_required_headers() + + # Security + self._check_security_schemes() + + return { + "valid": len(self.errors) == 0 and (not self.strict or len(self.warnings) == 0), + "errors": [e.to_dict() for e in self.errors], + "warnings": [w.to_dict() for w in self.warnings], + "guideline_version": "zalando-1.0", + "rules_checked": self._get_rules_checked() + } + + def _add_error(self, rule_id: str, message: str, path: str = None, suggestion: str = None): + """Add a validation error.""" + self.errors.append(ValidationError(rule_id, message, "error", path, suggestion)) + + def _add_warning(self, rule_id: str, message: str, path: str = None, suggestion: str = None): + """Add a validation warning.""" + if self.strict: + self.errors.append(ValidationError(rule_id, message, "error", path, suggestion)) + else: + self.warnings.append(ValidationError(rule_id, message, "warning", path, suggestion)) + + def _check_required_metadata(self): + """ + Check required metadata fields. + Zalando requires: x-api-id, x-audience + """ + info = self.spec.get("info", {}) + + # Check x-api-id (MUST) + if "x-api-id" not in info: + self._add_error( + "MUST_001", + "Missing required field 'info.x-api-id'", + "info.x-api-id", + "Add a UUID to uniquely identify this API: x-api-id: 'd0184f38-b98d-11e7-9c56-68f728c1ba70'" + ) + else: + # Validate UUID format + api_id = info["x-api-id"] + uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' + if not re.match(uuid_pattern, str(api_id), re.IGNORECASE): + self._add_error( + "MUST_001", + f"'info.x-api-id' must be a valid UUID, got: {api_id}", + "info.x-api-id" + ) + + # Check x-audience (MUST) + if "x-audience" not in info: + self._add_error( + "MUST_002", + "Missing required field 'info.x-audience'", + "info.x-audience", + "Specify target audience: x-audience: 'component-internal' | 'company-internal' | 'external-partner' | 'external-public'" + ) + else: + valid_audiences = ["component-internal", "company-internal", "external-partner", "external-public"] + audience = info["x-audience"] + if audience not in valid_audiences: + self._add_error( + "MUST_002", + f"'info.x-audience' must be one of: {', '.join(valid_audiences)}", + "info.x-audience" + ) + + # Check contact information (SHOULD) + if "contact" not in info: + self._add_warning( + "SHOULD_001", + "Missing 'info.contact' - should provide API owner contact information", + "info.contact", + "Add contact information: contact: {name: 'Team Name', email: 'team@company.com'}" + ) + + def _check_naming_conventions(self): + """ + Check naming conventions. + Zalando requires: snake_case for properties, kebab-case or snake_case for paths + """ + # Check path naming + paths = self.spec.get("paths", {}) + for path in paths.keys(): + # Remove path parameters for checking + path_without_params = re.sub(r'\{[^}]+\}', '', path) + segments = [s for s in path_without_params.split('/') if s] + + for segment in segments: + # Should be kebab-case or snake_case + if not re.match(r'^[a-z0-9_-]+$', segment): + self._add_error( + "MUST_003", + f"Path segment '{segment}' should use lowercase kebab-case or snake_case", + f"paths.{path}", + f"Use lowercase: {segment.lower()}" + ) + + # Check schema property naming (should be snake_case) + schemas = self.spec.get("components", {}).get("schemas", {}) + for schema_name, schema in schemas.items(): + if "properties" in schema and schema["properties"] is not None and isinstance(schema["properties"], dict): + for prop_name in schema["properties"].keys(): + if not re.match(r'^[a-z][a-z0-9_]*$', prop_name): + self._add_error( + "MUST_004", + f"Property '{prop_name}' in schema '{schema_name}' should use snake_case", + f"components.schemas.{schema_name}.properties.{prop_name}", + f"Use snake_case: {self._to_snake_case(prop_name)}" + ) + + def _check_http_methods(self): + """ + Check HTTP methods are used correctly. + """ + paths = self.spec.get("paths", {}) + for path, path_item in paths.items(): + for method in path_item.keys(): + if method.upper() not in ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "PARAMETERS"]: + continue + + operation = path_item[method] + + # GET should not have requestBody + if method.upper() == "GET" and "requestBody" in operation: + self._add_error( + "MUST_005", + f"GET operation should not have requestBody", + f"paths.{path}.get.requestBody" + ) + + # POST should return 201 for resource creation + if method.upper() == "POST": + responses = operation.get("responses", {}) + if "201" not in responses and "200" not in responses: + self._add_warning( + "SHOULD_002", + "POST operation should return 201 (Created) for resource creation", + f"paths.{path}.post.responses" + ) + + def _check_status_codes(self): + """ + Check proper use of HTTP status codes. + """ + paths = self.spec.get("paths", {}) + for path, path_item in paths.items(): + for method, operation in path_item.items(): + if method.upper() not in ["GET", "POST", "PUT", "PATCH", "DELETE"]: + continue + + responses = operation.get("responses", {}) + + # All operations should document error responses + if "400" not in responses: + self._add_warning( + "SHOULD_003", + f"{method.upper()} operation should document 400 (Bad Request) response", + f"paths.{path}.{method}.responses" + ) + + if "500" not in responses: + self._add_warning( + "SHOULD_004", + f"{method.upper()} operation should document 500 (Internal Error) response", + f"paths.{path}.{method}.responses" + ) + + # Check 201 has Location header + if "201" in responses: + response_201 = responses["201"] + headers = response_201.get("headers", {}) + if "Location" not in headers: + self._add_warning( + "SHOULD_005", + "201 (Created) response should include Location header", + f"paths.{path}.{method}.responses.201.headers", + "Add: headers: {Location: {schema: {type: string, format: uri}}}" + ) + + def _check_error_responses(self): + """ + Check error responses use RFC 7807 Problem JSON. + Zalando requires: application/problem+json for errors + """ + # Check if Problem schema exists + schemas = self.spec.get("components", {}).get("schemas", {}) + has_problem_schema = "Problem" in schemas + + if not has_problem_schema: + self._add_warning( + "SHOULD_006", + "Missing 'Problem' schema for RFC 7807 error responses", + "components.schemas", + "Add Problem schema following RFC 7807: https://datatracker.ietf.org/doc/html/rfc7807" + ) + + # Check error responses use application/problem+json + paths = self.spec.get("paths", {}) + for path, path_item in paths.items(): + for method, operation in path_item.items(): + if method.upper() not in ["GET", "POST", "PUT", "PATCH", "DELETE"]: + continue + + responses = operation.get("responses", {}) + for status_code, response in responses.items(): + # Check 4xx and 5xx responses + if status_code.startswith("4") or status_code.startswith("5"): + content = response.get("content", {}) + if content and "application/problem+json" not in content: + self._add_warning( + "SHOULD_007", + f"Error response {status_code} should use 'application/problem+json' content type", + f"paths.{path}.{method}.responses.{status_code}.content" + ) + + def _check_required_headers(self): + """ + Check for required headers. + Zalando requires: X-Flow-ID for request tracing + """ + # Check if responses document X-Flow-ID + paths = self.spec.get("paths", {}) + missing_flow_id = [] + + for path, path_item in paths.items(): + for method, operation in path_item.items(): + if method.upper() not in ["GET", "POST", "PUT", "PATCH", "DELETE"]: + continue + + responses = operation.get("responses", {}) + for status_code, response in responses.items(): + if status_code.startswith("2"): # Success responses + headers = response.get("headers", {}) + if "X-Flow-ID" not in headers and "X-Flow-Id" not in headers: + missing_flow_id.append(f"paths.{path}.{method}.responses.{status_code}") + + if missing_flow_id and len(missing_flow_id) > 0: + self._add_warning( + "SHOULD_008", + f"Success responses should include X-Flow-ID header for request tracing", + missing_flow_id[0], + "Add: headers: {X-Flow-ID: {schema: {type: string, format: uuid}}}" + ) + + def _check_security_schemes(self): + """ + Check security schemes are defined. + """ + components = self.spec.get("components", {}) + security_schemes = components.get("securitySchemes", {}) + + if not security_schemes: + self._add_warning( + "SHOULD_009", + "No security schemes defined - consider adding authentication", + "components.securitySchemes", + "Add security schemes: bearerAuth, oauth2, etc." + ) + + def _get_rules_checked(self) -> List[str]: + """Get list of rules that were checked.""" + return [ + "MUST_001: Required x-api-id metadata", + "MUST_002: Required x-audience metadata", + "MUST_003: Path naming conventions", + "MUST_004: Property naming conventions (snake_case)", + "MUST_005: HTTP method usage", + "SHOULD_001: Contact information", + "SHOULD_002: POST returns 201", + "SHOULD_003: Document 400 errors", + "SHOULD_004: Document 500 errors", + "SHOULD_005: 201 includes Location header", + "SHOULD_006: Problem schema for errors", + "SHOULD_007: Error responses use application/problem+json", + "SHOULD_008: X-Flow-ID header for tracing", + "SHOULD_009: Security schemes defined" + ] + + @staticmethod + def _to_snake_case(text: str) -> str: + """Convert text to snake_case.""" + # Insert underscore before uppercase letters + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) + # Insert underscore before uppercase letters preceded by lowercase + s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) + return s2.lower() diff --git a/skills/artifact.create/README.md b/skills/artifact.create/README.md new file mode 100644 index 0000000..9f18536 --- /dev/null +++ b/skills/artifact.create/README.md @@ -0,0 +1,241 @@ +# artifact.create + +## ⚙️ **Integration Note: Claude Code Plugin** + +**This skill is a Claude Code plugin.** You do not invoke it via `python skills/artifact.create/artifact_create.py`. Instead: + +- **Ask Claude Code** to use the skill: `"Use artifact.create to create a threat-model artifact..."` +- **Claude Code handles** validation, execution, and output interpretation +- **Direct Python execution** is only for development/testing outside Claude Code + +--- + +AI-assisted artifact generation from professional templates. + +## Purpose + +The `artifact.create` skill enables rapid, high-quality artifact creation by: + +1. Loading pre-built templates for 406+ artifact types +2. Populating templates with user-provided business context +3. Applying metadata and document control standards +4. Generating professional, ready-to-review artifacts + +## Usage + +### Via Claude Code (Recommended) + +Simply ask Claude to use the skill: + +``` +"Use artifact.create to create a business-case artifact for a new customer portal +that improves self-service capabilities and reduces support costs by 40%. +Save it to ./artifacts/customer-portal-business-case.yaml, +authored by Jane Smith, with Internal classification." + +"Use artifact.create to create a threat-model artifact for a payment processing API +with PCI-DSS compliance requirements. Save to ./artifacts/payment-api-threat-model.yaml, +authored by Security Team, with Confidential classification." + +"Use artifact.create to create a portfolio-roadmap artifact for a digital transformation +initiative covering cloud migration, API platform, and customer experience improvements +over 18 months. Save to ./artifacts/digital-transformation-roadmap.yaml, +authored by Strategy Office." +``` + +### Direct Execution (Development/Testing) + +When working outside Claude Code or for testing: + +```bash +python3 skills/artifact.create/artifact_create.py \ + \ + \ + \ + [--author "Your Name"] \ + [--classification Internal] +``` + +#### Examples + +**Create a Business Case:** +```bash +python3 skills/artifact.create/artifact_create.py \ + business-case \ + "New customer portal to improve self-service capabilities and reduce support costs by 40%" \ + ./artifacts/customer-portal-business-case.yaml \ + --author "Jane Smith" \ + --classification Internal +``` + +**Create a Threat Model:** +```bash +python3 skills/artifact.create/artifact_create.py \ + threat-model \ + "Payment processing API with PCI-DSS compliance requirements" \ + ./artifacts/payment-api-threat-model.yaml \ + --author "Security Team" \ + --classification Confidential +``` + +**Create a Portfolio Roadmap:** +```bash +python3 skills/artifact.create/artifact_create.py \ + portfolio-roadmap \ + "Digital transformation initiative covering cloud migration, API platform, and customer experience improvements over 18 months" \ + ./artifacts/digital-transformation-roadmap.yaml \ + --author "Strategy Office" +``` + +## How It Works + +### 1. Template Selection +- Validates artifact type against KNOWN_ARTIFACT_TYPES registry (406 types) +- Locates appropriate template in `templates/` directory +- Loads template structure (YAML or Markdown format) + +### 2. Metadata Population +- Substitutes placeholders: `{{date}}`, `{{your_name}}`, `{{role}}`, etc. +- Applies document control metadata (version, status, classification) +- Sets ownership and approval workflow metadata + +### 3. Context Integration +- **YAML artifacts**: Adds context hints within content sections +- **Markdown artifacts**: Inserts context at document beginning +- Preserves TODO markers for manual refinement + +### 4. Output Generation +- Creates output directory if needed +- Saves populated artifact to specified path +- Generates detailed report with next steps + +## Supported Artifact Types + +All 406 registered artifact types are supported, organized in 21 categories: + +| Category | Examples | +|----------|----------| +| **Governance** | business-case, portfolio-roadmap, raid-log, decision-log | +| **Architecture** | threat-model, logical-architecture-diagram, data-flow-diagram | +| **Data** | data-model, schema-definition, data-dictionary | +| **Testing** | test-plan, test-results, acceptance-criteria | +| **Security** | security-assessment, vulnerability-report, incident-response-plan | +| **Deployment** | deployment-plan, release-checklist, rollback-plan | +| **Requirements** | requirements-specification, use-case-diagram, user-story | +| **AI/ML** | model-card, training-dataset-description, model-evaluation-report | + +See `skills/artifact.define/artifact_define.py` for the complete list. + +## Output Formats + +### YAML Templates (312 artifacts) +Structured data artifacts for: +- Schemas, models, specifications +- Plans, roadmaps, matrices +- Configurations, manifests, definitions + +Example: `business-case.yaml`, `threat-model.yaml`, `data-model.yaml` + +### Markdown Templates (94 artifacts) +Documentation artifacts for: +- Reports, guides, manuals +- Policies, procedures, handbooks +- Assessments, analyses, reviews + +Example: `incident-report.md`, `runbook.md`, `architecture-guide.md` + +## Generated Artifact Structure + +Every generated artifact includes: + +- ✅ **Document Control**: Version, dates, author, status, classification +- ✅ **Ownership Metadata**: Document owner, approvers, approval workflow +- ✅ **Related Documents**: Links to upstream/downstream dependencies +- ✅ **Structured Content**: Context-aware sections with TODO guidance +- ✅ **Change History**: Version tracking with dates and authors +- ✅ **Reference Links**: Pointers to comprehensive artifact descriptions + +## Next Steps After Generation + +1. **Review** the generated artifact at the output path +2. **Consult** the comprehensive guidance in `artifact_descriptions/{artifact-type}.md` +3. **Replace** any remaining TODO markers with specific details +4. **Validate** the structure and content against requirements +5. **Update** metadata (status → Review → Approved → Published) +6. **Link** related documents in the metadata section + +## Integration with Artifact Framework + +### Artifact Metadata + +```yaml +artifact_metadata: + produces: + - type: "*" # Dynamically produces any registered artifact type + description: Generated from professional templates + file_pattern: "{{output_path}}" + content_type: application/yaml, text/markdown + + consumes: + - type: artifact-type-description + description: References comprehensive artifact descriptions + file_pattern: "artifact_descriptions/*.md" +``` + +### Workflow Integration + +``` +User Context → artifact.create → Generated Artifact → artifact.validate → artifact.review +``` + +Future skills: +- `artifact.validate`: Schema and quality validation +- `artifact.review`: AI-powered content review and recommendations + +## Error Handling + +### Unknown Artifact Type +``` +Error: Unknown artifact type: invalid-type +Available artifact types (showing first 10): + - business-case + - threat-model + - portfolio-roadmap + ... +``` + +### Missing Template +``` +Error: No template found for artifact type: custom-type +``` + +## Performance + +- **Template loading**: <50ms +- **Content population**: <200ms +- **Total generation time**: <1 second +- **Output size**: Typically 2-5 KB (YAML), 3-8 KB (Markdown) + +## Dependencies + +- Python 3.7+ +- `artifact.define` skill (for KNOWN_ARTIFACT_TYPES registry) +- Templates in `templates/` directory (406 templates) +- Artifact descriptions in `artifact_descriptions/` (391 files, ~160K lines) + +## Status + +**Active** - Phase 1 implementation complete + +## Tags + +artifacts, templates, generation, ai-assisted, tier2 + +## Version History + +- **0.1.0** (2024-10-25): Initial implementation + - Support for all 406 artifact types + - YAML and Markdown template population + - Metadata substitution + - Context integration + - Generation reporting diff --git a/skills/artifact.create/__init__.py b/skills/artifact.create/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/artifact.create/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/artifact.create/artifact_create.py b/skills/artifact.create/artifact_create.py new file mode 100755 index 0000000..3ba87fc --- /dev/null +++ b/skills/artifact.create/artifact_create.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +""" +artifact.create skill - AI-assisted artifact generation from templates + +Loads templates based on artifact type, populates them with user-provided context, +and generates professional, ready-to-use artifacts. +""" + +import sys +import os +import argparse +from pathlib import Path +from datetime import datetime +from typing import Dict, Any, Optional, Tuple +import re +import yaml + +# Add parent directory to path for governance imports +from betty.governance import enforce_governance, log_governance_action + + +def load_artifact_registry() -> Dict[str, Any]: + """Load artifact registry from artifact.define skill""" + registry_file = Path(__file__).parent.parent / "artifact.define" / "artifact_define.py" + + if not registry_file.exists(): + raise FileNotFoundError(f"Artifact registry not found: {registry_file}") + + with open(registry_file, 'r') as f: + content = f.read() + + # Find KNOWN_ARTIFACT_TYPES dictionary + start_marker = "KNOWN_ARTIFACT_TYPES = {" + start_idx = content.find(start_marker) + if start_idx == -1: + raise ValueError("Could not find KNOWN_ARTIFACT_TYPES in registry file") + + start_idx += len(start_marker) - 1 # Include the { + + # Find matching closing brace + brace_count = 0 + end_idx = start_idx + for i in range(start_idx, len(content)): + if content[i] == '{': + brace_count += 1 + elif content[i] == '}': + brace_count -= 1 + if brace_count == 0: + end_idx = i + 1 + break + + dict_str = content[start_idx:end_idx] + artifacts = eval(dict_str) # Safe since it's our own code + return artifacts + + +def find_template_path(artifact_type: str) -> Optional[Path]: + """Find the template file for a given artifact type""" + templates_dir = Path(__file__).parent.parent.parent / "templates" + + if not templates_dir.exists(): + raise FileNotFoundError(f"Templates directory not found: {templates_dir}") + + # Search all subdirectories for the template + for template_file in templates_dir.rglob(f"{artifact_type}.*"): + if template_file.is_file() and template_file.suffix in ['.yaml', '.yml', '.md']: + return template_file + + return None + + +def get_artifact_description_path(artifact_type: str) -> Optional[Path]: + """Get path to artifact description file for reference""" + desc_dir = Path(__file__).parent.parent.parent / "artifact_descriptions" + desc_file = desc_dir / f"{artifact_type}.md" + + if desc_file.exists(): + return desc_file + return None + + +def substitute_metadata(template_content: str, metadata: Optional[Dict[str, Any]] = None) -> str: + """Substitute metadata placeholders in template""" + if metadata is None: + metadata = {} + + # Default metadata + today = datetime.now().strftime("%Y-%m-%d") + defaults = { + 'date': today, + 'your_name': metadata.get('author', 'TODO: Add author name'), + 'role': metadata.get('role', 'TODO: Define role'), + 'approver_name': metadata.get('approver_name', 'TODO: Add approver name'), + 'approver_role': metadata.get('approver_role', 'TODO: Add approver role'), + 'artifact_type': metadata.get('artifact_type', 'TODO: Specify artifact type'), + 'path': metadata.get('path', 'TODO: Add path'), + } + + # Override with provided metadata + defaults.update(metadata) + + # Perform substitutions + result = template_content + for key, value in defaults.items(): + result = result.replace(f"{{{{{key}}}}}", str(value)) + + return result + + +def populate_yaml_template(template_content: str, context: str, artifact_type: str) -> str: + """Populate YAML template with context-aware content""" + + # Parse the template to understand structure + lines = template_content.split('\n') + result_lines = [] + in_content_section = False + + for line in lines: + # Check if we're entering the content section + if line.strip().startswith('content:') or line.strip().startswith('# Content'): + in_content_section = True + result_lines.append(line) + continue + + # If we're in content section and find a TODO, replace with context hint + if in_content_section and 'TODO:' in line: + indent = len(line) - len(line.lstrip()) + # Keep the TODO but add a hint about using the context + result_lines.append(line) + result_lines.append(f"{' ' * indent}# Context provided: {context[:100]}...") + else: + result_lines.append(line) + + return '\n'.join(result_lines) + + +def populate_markdown_template(template_content: str, context: str, artifact_type: str) -> str: + """Populate Markdown template with context-aware content""" + + # Add context as a note in the document + lines = template_content.split('\n') + result_lines = [] + + # Find the first heading and add context after it + first_heading_found = False + for line in lines: + result_lines.append(line) + + if not first_heading_found and line.startswith('# '): + first_heading_found = True + result_lines.append('') + result_lines.append(f'> **Context**: {context}') + result_lines.append('') + + return '\n'.join(result_lines) + + +def load_existing_artifact_metadata(artifact_path: Path) -> Optional[Dict[str, Any]]: + """ + Load metadata from an existing artifact file. + + Args: + artifact_path: Path to the existing artifact file + + Returns: + Dictionary containing artifact metadata, or None if file doesn't exist + """ + if not artifact_path.exists(): + return None + + try: + with open(artifact_path, 'r') as f: + content = f.read() + + # Try to parse as YAML first + try: + data = yaml.safe_load(content) + if isinstance(data, dict) and 'metadata' in data: + return data['metadata'] + except yaml.YAMLError: + pass + + # If YAML parsing fails or no metadata found, return None + return None + + except Exception as e: + # If we can't read the file, return None + return None + + +def generate_artifact( + artifact_type: str, + context: str, + output_path: str, + metadata: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: + """ + Generate an artifact from template with AI-assisted population + + Args: + artifact_type: Type of artifact (must exist in KNOWN_ARTIFACT_TYPES) + context: Business context for populating the artifact + output_path: Where to save the generated artifact + metadata: Optional metadata overrides + + Returns: + Generation report with status, path, and details + """ + + # Validate artifact type + artifacts = load_artifact_registry() + if artifact_type not in artifacts: + return { + 'success': False, + 'error': f"Unknown artifact type: {artifact_type}", + 'available_types': list(artifacts.keys())[:10] # Show first 10 as hint + } + + # Find template + template_path = find_template_path(artifact_type) + if not template_path: + return { + 'success': False, + 'error': f"No template found for artifact type: {artifact_type}", + 'artifact_type': artifact_type + } + + # Load template + with open(template_path, 'r') as f: + template_content = f.read() + + # Determine format + artifact_format = template_path.suffix.lstrip('.') + + # Substitute metadata placeholders + populated_content = substitute_metadata(template_content, metadata) + + # Populate with context + if artifact_format in ['yaml', 'yml']: + populated_content = populate_yaml_template(populated_content, context, artifact_type) + elif artifact_format == 'md': + populated_content = populate_markdown_template(populated_content, context, artifact_type) + + # Ensure output directory exists + output_file = Path(output_path) + output_file.parent.mkdir(parents=True, exist_ok=True) + + # Governance check: Enforce governance on existing artifact before overwriting + existing_metadata = load_existing_artifact_metadata(output_file) + if existing_metadata: + try: + # Add artifact ID if not present + if 'id' not in existing_metadata: + existing_metadata['id'] = str(output_file) + + enforce_governance(existing_metadata) + + # Log successful governance check + log_governance_action( + artifact_id=existing_metadata.get('id', str(output_file)), + action="write", + outcome="allowed", + message="Governance check passed, allowing artifact update", + metadata={ + 'artifact_type': artifact_type, + 'output_path': str(output_file) + } + ) + + except PermissionError as e: + # Governance policy violation - return error + return { + 'success': False, + 'error': f"Governance policy violation: {str(e)}", + 'artifact_type': artifact_type, + 'policy_violation': True, + 'existing_metadata': existing_metadata + } + except ValueError as e: + # Invalid metadata - log warning but allow write + log_governance_action( + artifact_id=str(output_file), + action="write", + outcome="warning", + message=f"Invalid metadata in existing artifact: {str(e)}", + metadata={ + 'artifact_type': artifact_type, + 'output_path': str(output_file) + } + ) + + # Save generated artifact + with open(output_file, 'w') as f: + f.write(populated_content) + + # Get artifact description path for reference + desc_path = get_artifact_description_path(artifact_type) + + # Generate report + report = { + 'success': True, + 'artifact_file': str(output_file.absolute()), + 'artifact_type': artifact_type, + 'artifact_format': artifact_format, + 'template_used': str(template_path.name), + 'artifact_description': str(desc_path) if desc_path else None, + 'context_length': len(context), + 'generated_at': datetime.now().isoformat(), + 'next_steps': [ + f"Review the generated artifact at: {output_file}", + f"Refer to comprehensive guidance at: {desc_path}" if desc_path else "Review and customize the content", + "Replace any remaining TODO markers with specific information", + "Validate the artifact structure and content", + "Update metadata (status, approvers, etc.) as needed" + ] + } + + return report + + +def main(): + """Main entry point for artifact.create skill""" + parser = argparse.ArgumentParser( + description='Create artifacts from templates with AI-assisted population' + ) + parser.add_argument( + 'artifact_type', + type=str, + help='Type of artifact to create (e.g., business-case, threat-model)' + ) + parser.add_argument( + 'context', + type=str, + help='Business context for populating the artifact' + ) + parser.add_argument( + 'output_path', + type=str, + help='Path where the generated artifact should be saved' + ) + parser.add_argument( + '--author', + type=str, + help='Author name for metadata' + ) + parser.add_argument( + '--classification', + type=str, + choices=['Public', 'Internal', 'Confidential', 'Restricted'], + help='Document classification level' + ) + + args = parser.parse_args() + + # Build metadata from arguments + metadata = {} + if args.author: + metadata['author'] = args.author + metadata['your_name'] = args.author + if args.classification: + metadata['classification'] = args.classification + + # Generate artifact + report = generate_artifact( + artifact_type=args.artifact_type, + context=args.context, + output_path=args.output_path, + metadata=metadata if metadata else None + ) + + # Print report + if report['success']: + print(f"\n{'='*70}") + print(f"✓ Artifact Generated Successfully") + print(f"{'='*70}") + print(f"Type: {report['artifact_type']}") + print(f"Format: {report['artifact_format']}") + print(f"Output: {report['artifact_file']}") + if report.get('artifact_description'): + print(f"Guide: {report['artifact_description']}") + print(f"\nNext Steps:") + for i, step in enumerate(report['next_steps'], 1): + print(f" {i}. {step}") + print(f"{'='*70}\n") + return 0 + else: + print(f"\n{'='*70}") + print(f"✗ Artifact Generation Failed") + print(f"{'='*70}") + print(f"Error: {report['error']}") + if 'available_types' in report: + print(f"\nAvailable artifact types (showing first 10):") + for atype in report['available_types']: + print(f" - {atype}") + print(f" ... and more") + print(f"{'='*70}\n") + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/skills/artifact.create/skill.yaml b/skills/artifact.create/skill.yaml new file mode 100644 index 0000000..f4d67f5 --- /dev/null +++ b/skills/artifact.create/skill.yaml @@ -0,0 +1,94 @@ +name: artifact.create +version: 0.1.0 +description: > + Create artifacts from templates with AI-assisted population. Takes an artifact type + and business context, loads the appropriate template, and generates a complete, + professional artifact ready for review and use. + +inputs: + - name: artifact_type + type: string + required: true + description: Type of artifact to create (e.g., "business-case", "threat-model", "portfolio-roadmap") + + - name: context + type: string + required: true + description: Business context, requirements, and information to populate the artifact + + - name: output_path + type: string + required: true + description: Path where the generated artifact should be saved + + - name: metadata + type: object + required: false + description: Optional metadata overrides (author, classification, approvers, etc.) + +outputs: + - name: artifact_file + type: string + description: Path to the generated artifact file + + - name: artifact_format + type: string + description: Format of the generated artifact (yaml or markdown) + + - name: generation_report + type: object + description: Report on the generation process, including populated sections and validation status + +dependencies: + - artifact.define + +entrypoints: + - command: /skill/artifact/create + handler: artifact_create.py + runtime: python + description: > + Generate artifacts from templates with AI assistance. Loads the appropriate + template based on artifact type, populates it with provided context using + intelligent content generation, and saves the result to the specified path. + parameters: + - name: artifact_type + type: string + required: true + description: Artifact type (must exist in KNOWN_ARTIFACT_TYPES) + - name: context + type: string + required: true + description: Business context for populating the artifact + - name: output_path + type: string + required: true + description: Output file path + - name: metadata + type: object + required: false + description: Metadata overrides (author, approvers, etc.) + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: + - artifacts + - templates + - generation + - ai-assisted + - tier2 + +# This skill's own artifact metadata +artifact_metadata: + produces: + - type: "*" + description: Dynamically produces any registered artifact type based on artifact_type parameter + file_pattern: "{{output_path}}" + content_type: application/yaml, text/markdown + + consumes: + - type: artifact-type-description + description: References artifact descriptions for guidance on structure and content + file_pattern: "artifact_descriptions/*.md" diff --git a/skills/artifact.define/__init__.py b/skills/artifact.define/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/artifact.define/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/artifact.define/artifact_define.py b/skills/artifact.define/artifact_define.py new file mode 100644 index 0000000..6beca0c --- /dev/null +++ b/skills/artifact.define/artifact_define.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +artifact_define.py - Define artifact metadata for Betty Framework skills + +Helps create artifact_metadata blocks that declare what artifacts a skill +produces and consumes, enabling interoperability. +""" + +import os +import sys +import json +import yaml +from typing import Dict, Any, List, Optional +from pathlib import Path + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + +# Known artifact types - loaded from registry/artifact_types.json +# To update the registry, modify registry/artifact_types.json and reload +from skills.artifact.define.registry_loader import ( + KNOWN_ARTIFACT_TYPES, + load_artifact_registry, + reload_registry, + get_artifact_count, + is_registered, + get_artifact_metadata +) + +# For backward compatibility, expose the registry as module-level variable +# Note: The registry is now data-driven and loaded from JSON +# Do not modify this file to add new artifact types +# Instead, update registry/artifact_types.json + + + + + +def get_artifact_definition(artifact_type: str) -> Optional[Dict[str, Any]]: + """ + Get the definition for a known artifact type. + + Args: + artifact_type: Artifact type identifier + + Returns: + Artifact definition dictionary with schema, file_pattern, etc., or None if unknown + """ + if artifact_type in KNOWN_ARTIFACT_TYPES: + definition = {"type": artifact_type} + definition.update(KNOWN_ARTIFACT_TYPES[artifact_type]) + return definition + return None + + +def validate_artifact_type(artifact_type: str) -> tuple[bool, Optional[str]]: + """ + Validate that an artifact type is known or suggest registering it. + + Args: + artifact_type: Artifact type identifier + + Returns: + Tuple of (is_valid, warning_message) + """ + if artifact_type in KNOWN_ARTIFACT_TYPES: + return True, None + + warning = f"Artifact type '{artifact_type}' is not in the known registry. " + warning += "Consider documenting it in docs/ARTIFACT_STANDARDS.md and creating a schema." + return False, warning + + +def generate_artifact_metadata( + skill_name: str, + produces: Optional[List[str]] = None, + consumes: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Generate artifact metadata structure. + + Args: + skill_name: Name of the skill + produces: List of artifact types produced + consumes: List of artifact types consumed + + Returns: + Artifact metadata dictionary + """ + metadata = {} + warnings = [] + + # Build produces section + if produces: + produces_list = [] + for artifact_type in produces: + is_known, warning = validate_artifact_type(artifact_type) + if warning: + warnings.append(warning) + + artifact_def = {"type": artifact_type} + + # Add known metadata if available + if artifact_type in KNOWN_ARTIFACT_TYPES: + known = KNOWN_ARTIFACT_TYPES[artifact_type] + if "schema" in known: + artifact_def["schema"] = known["schema"] + if "file_pattern" in known: + artifact_def["file_pattern"] = known["file_pattern"] + if "content_type" in known: + artifact_def["content_type"] = known["content_type"] + if "description" in known: + artifact_def["description"] = known["description"] + + produces_list.append(artifact_def) + + metadata["produces"] = produces_list + + # Build consumes section + if consumes: + consumes_list = [] + for artifact_type in consumes: + is_known, warning = validate_artifact_type(artifact_type) + if warning: + warnings.append(warning) + + artifact_def = { + "type": artifact_type, + "required": True # Default to required + } + + # Add description if known + if artifact_type in KNOWN_ARTIFACT_TYPES: + known = KNOWN_ARTIFACT_TYPES[artifact_type] + if "description" in known: + artifact_def["description"] = known["description"] + + consumes_list.append(artifact_def) + + metadata["consumes"] = consumes_list + + return metadata, warnings + + +def format_as_yaml(metadata: Dict[str, Any]) -> str: + """ + Format artifact metadata as YAML for inclusion in skill.yaml. + + Args: + metadata: Artifact metadata dictionary + + Returns: + Formatted YAML string + """ + yaml_str = "artifact_metadata:\n" + yaml_str += yaml.dump(metadata, default_flow_style=False, indent=2, sort_keys=False) + return yaml_str + + +def main(): + """CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Define artifact metadata for Betty Framework skills" + ) + parser.add_argument( + "skill_name", + help="Name of the skill (e.g., api.define)" + ) + parser.add_argument( + "--produces", + nargs="+", + help="Artifact types this skill produces" + ) + parser.add_argument( + "--consumes", + nargs="+", + help="Artifact types this skill consumes" + ) + parser.add_argument( + "--output-file", + default="artifact_metadata.yaml", + help="Output file path" + ) + + args = parser.parse_args() + + logger.info(f"Generating artifact metadata for skill: {args.skill_name}") + + try: + # Generate metadata + metadata, warnings = generate_artifact_metadata( + args.skill_name, + produces=args.produces, + consumes=args.consumes + ) + + # Format as YAML + yaml_content = format_as_yaml(metadata) + + # Save to file + output_path = args.output_file + with open(output_path, 'w') as f: + f.write(yaml_content) + + logger.info(f"✅ Generated artifact metadata: {output_path}") + + # Print to stdout + print("\n# Add this to your skill.yaml:\n") + print(yaml_content) + + # Show warnings + if warnings: + logger.warning("\n⚠️ Warnings:") + for warning in warnings: + logger.warning(f" - {warning}") + + # Print summary + logger.info("\n📋 Summary:") + if metadata.get("produces"): + logger.info(f" Produces: {', '.join(a['type'] for a in metadata['produces'])}") + if metadata.get("consumes"): + logger.info(f" Consumes: {', '.join(a['type'] for a in metadata['consumes'])}") + + # Success result + result = { + "ok": True, + "status": "success", + "skill_name": args.skill_name, + "metadata": metadata, + "output_file": output_path, + "warnings": warnings + } + + print("\n" + json.dumps(result, indent=2)) + sys.exit(0) + + except Exception as e: + logger.error(f"Failed to generate artifact metadata: {e}") + result = { + "ok": False, + "status": "failed", + "error": str(e) + } + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/artifact.define/artifact_define.py.backup b/skills/artifact.define/artifact_define.py.backup new file mode 100644 index 0000000..840f55e --- /dev/null +++ b/skills/artifact.define/artifact_define.py.backup @@ -0,0 +1,2272 @@ +#!/usr/bin/env python3 +""" +artifact_define.py - Define artifact metadata for Betty Framework skills + +Helps create artifact_metadata blocks that declare what artifacts a skill +produces and consumes, enabling interoperability. +""" + +import os +import sys +import json +import yaml +from typing import Dict, Any, List, Optional +from pathlib import Path + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + +# Known artifact types and their metadata +KNOWN_ARTIFACT_TYPES = { + "openapi-spec": { + "schema": "schemas/openapi-spec.json", + "file_pattern": "*.openapi.yaml", + "content_type": "application/yaml", + "description": "OpenAPI 3.0+ specification for Architecture and API Development. Part of Application and Integration documentation and deliverables." + }, + "validation-report": { + "schema": "schemas/validation-report.json", + "file_pattern": "*.validation.json", + "content_type": "application/json", + "description": "Structured validation results for Testing and Quality Assurance. Part of Quality Assurance documentation and deliverables." + }, + "workflow-definition": { + "schema": "schemas/workflow-definition.json", + "file_pattern": "*.workflow.yaml", + "content_type": "application/yaml", + "description": "Betty workflow definition for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "hook-config": { + "schema": "schemas/hook-config.json", + "file_pattern": "hooks.yaml", + "content_type": "application/yaml", + "description": "Claude Code hook configuration for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "api-models": { + "file_pattern": "*.{py,ts,go}", + "description": "Generated API data models for Implementation and Development. Part of Development documentation and deliverables." + }, + "agent-description": { + "schema": "schemas/agent-description.json", + "file_pattern": "**/agent_description.md", + "content_type": "text/markdown", + "description": "Natural language description of agent purpose and requirements for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "agent-definition": { + "schema": "schemas/agent-definition.json", + "file_pattern": "agents/*/agent.yaml", + "content_type": "application/yaml", + "description": "Complete agent configuration with skills and metadata for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "agent-documentation": { + "file_pattern": "agents/*/README.md", + "content_type": "text/markdown", + "description": "Human-readable agent documentation for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "optimization-report": { + "schema": "schemas/optimization-report.json", + "file_pattern": "*.optimization.json", + "content_type": "application/json", + "description": "Performance and security optimization recommendations for Architecture and Performance Engineering. Part of Application and Integration documentation and deliverables." + }, + "compatibility-graph": { + "schema": "schemas/compatibility-graph.json", + "file_pattern": "*.compatibility.json", + "content_type": "application/json", + "description": "Agent relationship graph showing which agents can work together for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "pipeline-suggestion": { + "schema": "schemas/pipeline-suggestion.json", + "file_pattern": "*.pipeline.json", + "content_type": "application/json", + "description": "Suggested multi-agent workflow with step-by-step execution plan for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "suggestion-report": { + "schema": "schemas/suggestion-report.json", + "file_pattern": "*.suggestions.json", + "content_type": "application/json", + "description": "Context-aware recommendations for what to do next for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "skill-description": { + "schema": "schemas/skill-description.json", + "file_pattern": "**/skill_description.md", + "content_type": "text/markdown", + "description": "Natural language description of a skill's requirements, inputs, outputs, and implementation details for Implementation and Development. Part of Development documentation and deliverables." + }, + "agile-epic": { + "file_pattern": "*.epic.md", + "content_type": "text/markdown", + "description": "Agile Epic document with structured fields for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "user-stories-list": { + "file_pattern": "*.stories.json", + "content_type": "application/json", + "description": "Structured JSON list of user story summaries for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "user-story": { + "file_pattern": "story_*.md", + "content_type": "text/markdown", + "description": "Fully formatted user story document following standard format for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "skill-definition": { + "schema": "schemas/skill-definition.json", + "file_pattern": "skills/*/skill.yaml", + "content_type": "application/yaml", + "description": "Complete skill configuration in YAML format for Implementation and Development. Part of Development documentation and deliverables." + }, + "hook-description": { + "schema": "schemas/hook-description.json", + "file_pattern": "**/hook_description.md", + "content_type": "text/markdown", + "description": "Natural language description of a Claude Code hook's purpose, trigger event, and command to execute for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "acceptable-use-policy": { + "file_pattern": "*.acceptable-use-policy.md", + "content_type": "text/markdown", + "description": "Acceptable Use Policy for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "acceptance-criteria": { + "file_pattern": "*.acceptance-criteria.md", + "content_type": "text/markdown", + "description": "Acceptance criteria for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "access-recertification-plan": { + "file_pattern": "*.access-recertification-plan.md", + "content_type": "text/markdown", + "description": "Access recertification plan for Architecture. Part of Security Architecture documentation and deliverables." + }, + "access-review-logs": { + "file_pattern": "*.access-review-logs.md", + "content_type": "text/markdown", + "description": "Access review logs for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "accessibility-audits": { + "file_pattern": "*.accessibility-audits.md", + "content_type": "text/markdown", + "description": "Accessibility audits for Design. Part of Design & UX documentation and deliverables." + }, + "accessibility-requirements": { + "file_pattern": "*.accessibility-requirements.md", + "content_type": "text/markdown", + "description": "Accessibility requirements for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "admin-guides": { + "file_pattern": "*.admin-guides.md", + "content_type": "text/markdown", + "description": "Admin guides for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "adr-index": { + "file_pattern": "*.adr-index.md", + "content_type": "text/markdown", + "description": "ADR index for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "adversary-emulation-documents": { + "file_pattern": "*.adversary-emulation-documents.md", + "content_type": "text/markdown", + "description": "Adversary emulation documents for Architecture. Part of Security Architecture documentation and deliverables." + }, + "ai-ethics-and-bias-assessment": { + "file_pattern": "*.ai-ethics-and-bias-assessment.md", + "content_type": "text/markdown", + "description": "AI ethics and bias assessment for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "ai-use-case-inventory": { + "file_pattern": "*.ai-use-case-inventory.md", + "content_type": "text/markdown", + "description": "AI use-case inventory for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "alert-catalogs": { + "file_pattern": "*.alert-catalogs.md", + "content_type": "text/markdown", + "description": "Alert catalogs for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "analytics-model-documentation": { + "file_pattern": "*.analytics-model-documentation.md", + "content_type": "text/markdown", + "description": "Analytics model documentation for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "api-catalogs": { + "file_pattern": "*.api-catalogs.md", + "content_type": "text/markdown", + "description": "API catalogs for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "api-versioning-policy": { + "file_pattern": "*.api-versioning-policy.md", + "content_type": "text/markdown", + "description": "API versioning policy for Architecture. Part of Application and Integration documentation and deliverables." + }, + "app-store-metadata": { + "file_pattern": "*.app-store-metadata.md", + "content_type": "text/markdown", + "description": "App store metadata for Mobile, Desktop, and Distribution. Part of Client Distribution documentation and deliverables." + }, + "approval-evidence": { + "file_pattern": "*.approval-evidence.md", + "content_type": "text/markdown", + "description": "Approval evidence for HR, Access, and Lifecycle. Part of Access & Identity documentation and deliverables." + }, + "architecture-approvals": { + "file_pattern": "*.architecture-approvals.md", + "content_type": "text/markdown", + "description": "Architecture approvals for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "architecture-overview": { + "file_pattern": "*.architecture-overview.md", + "content_type": "text/markdown", + "description": "Architecture overview for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "architecture-review-board-minutes": { + "file_pattern": "*.architecture-review-board-minutes.md", + "content_type": "text/markdown", + "description": "Architecture review board minutes for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "architecture-vision": { + "file_pattern": "*.architecture-vision.md", + "content_type": "text/markdown", + "description": "Architecture vision for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "architecture-waivers": { + "file_pattern": "*.architecture-waivers.md", + "content_type": "text/markdown", + "description": "Architecture waivers for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "archival-plan": { + "file_pattern": "*.archival-plan.md", + "content_type": "text/markdown", + "description": "Archival plan for Closure and Archival. Part of Project Closure documentation and deliverables." + }, + "artifact-registry-policies": { + "file_pattern": "*.artifact-registry-policies.md", + "content_type": "text/markdown", + "description": "Artifact registry policies for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "artifact-store-policies": { + "file_pattern": "*.artifact-store-policies.md", + "content_type": "text/markdown", + "description": "Artifact store policies for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "asyncapi-specification": { + "file_pattern": "*.asyncapi-specification.md", + "content_type": "text/markdown", + "description": "AsyncAPI specification for Architecture. Part of Application and Integration documentation and deliverables." + }, + "attribution-files": { + "file_pattern": "*.attribution-files.md", + "content_type": "text/markdown", + "description": "Attribution files for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "audit-readiness-workbook": { + "file_pattern": "*.audit-readiness-workbook.md", + "content_type": "text/markdown", + "description": "Audit readiness workbook for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "auto-update-policies": { + "file_pattern": "*.auto-update-policies.md", + "content_type": "text/markdown", + "description": "Auto-update policies for Mobile, Desktop, and Distribution. Part of Client Distribution documentation and deliverables." + }, + "automated-quality-gates": { + "file_pattern": "*.automated-quality-gates.md", + "content_type": "text/markdown", + "description": "Automated quality gates for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "automated-test-scripts": { + "file_pattern": "*.automated-test-scripts.txt", + "content_type": "text/plain", + "description": "Automated test scripts for Testing. Part of Quality Assurance documentation and deliverables." + }, + "backup-and-recovery-plan": { + "file_pattern": "*.backup-and-recovery-plan.md", + "content_type": "text/markdown", + "description": "Backup and recovery plan for Architecture. Part of Data and Information documentation and deliverables." + }, + "backup-verification-logs": { + "file_pattern": "*.backup-verification-logs.md", + "content_type": "text/markdown", + "description": "Backup verification logs for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "baseline-hardening-guides": { + "file_pattern": "*.baseline-hardening-guides.md", + "content_type": "text/markdown", + "description": "Baseline hardening guides for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "battlecards": { + "file_pattern": "*.battlecards.md", + "content_type": "text/markdown", + "description": "Battlecards for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "benefits-realization-plan": { + "file_pattern": "*.benefits-realization-plan.md", + "content_type": "text/markdown", + "description": "Benefits realization plan for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "benefits-realization-report": { + "file_pattern": "*.benefits-realization-report.md", + "content_type": "text/markdown", + "description": "Benefits realization report for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "bias-and-fairness-reports": { + "file_pattern": "*.bias-and-fairness-reports.md", + "content_type": "text/markdown", + "description": "Bias and fairness reports for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "bounded-context-map": { + "file_pattern": "*.bounded-context-map.*", + "description": "Bounded context map for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "budget-forecast": { + "file_pattern": "*.budget-forecast.md", + "content_type": "text/markdown", + "description": "Budget forecast for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "bug-bounty-brief": { + "file_pattern": "*.bug-bounty-brief.md", + "content_type": "text/markdown", + "description": "Bug bounty brief for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "build-reproducibility-notes": { + "file_pattern": "*.build-reproducibility-notes.md", + "content_type": "text/markdown", + "description": "Build reproducibility notes for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "build-scripts": { + "file_pattern": "*.build-scripts.txt", + "content_type": "text/plain", + "description": "Build scripts for Implementation. Part of Development documentation and deliverables." + }, + "business-associate-agreement": { + "file_pattern": "*.business-associate-agreement.md", + "content_type": "text/markdown", + "description": "Business Associate Agreement (BAA) for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "business-case": { + "file_pattern": "*.business-case.md", + "content_type": "text/markdown", + "description": "Business case for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "business-process-models": { + "file_pattern": "*.business-process-models.*", + "description": "Business process models (BPMN, flowcharts) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "business-rules-catalog": { + "file_pattern": "*.business-rules-catalog.md", + "content_type": "text/markdown", + "description": "Business rules catalog for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "cab-approvals": { + "file_pattern": "*.cab-approvals.md", + "content_type": "text/markdown", + "description": "CAB approvals for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "caching-strategy": { + "file_pattern": "*.caching-strategy.md", + "content_type": "text/markdown", + "description": "Caching strategy for Design. Part of Design & UX documentation and deliverables." + }, + "caching-tiers": { + "file_pattern": "*.caching-tiers.md", + "content_type": "text/markdown", + "description": "Caching tiers for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "capability-model": { + "file_pattern": "*.capability-model.md", + "content_type": "text/markdown", + "description": "Capability model for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "capacity-models": { + "file_pattern": "*.capacity-models.md", + "content_type": "text/markdown", + "description": "Capacity models for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "capacity-plan": { + "file_pattern": "*.capacity-plan.md", + "content_type": "text/markdown", + "description": "Capacity plan for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "capitalization-policy": { + "file_pattern": "*.capitalization-policy.md", + "content_type": "text/markdown", + "description": "Capitalization policy for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "carbon-footprint-analysis": { + "file_pattern": "*.carbon-footprint-analysis.md", + "content_type": "text/markdown", + "description": "Carbon footprint analysis for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "cdn-and-waf-configs": { + "file_pattern": "*.cdn-and-waf-configs.md", + "content_type": "text/markdown", + "description": "CDN and WAF configs for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "certificate-policy": { + "file_pattern": "*.certificate-policy.md", + "content_type": "text/markdown", + "description": "Certificate policy for Architecture. Part of Security Architecture documentation and deliverables." + }, + "certification-exams": { + "file_pattern": "*.certification-exams.md", + "content_type": "text/markdown", + "description": "Certification exams for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "change-control-plan": { + "file_pattern": "*.change-control-plan.md", + "content_type": "text/markdown", + "description": "Change control plan for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "change-log": { + "file_pattern": "*.change-log.md", + "content_type": "text/markdown", + "description": "Change log for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "changelogs": { + "file_pattern": "*.changelogs.md", + "content_type": "text/markdown", + "description": "Changelogs for Implementation. Part of Development documentation and deliverables." + }, + "chaos-engineering-experiments": { + "file_pattern": "*.chaos-engineering-experiments.md", + "content_type": "text/markdown", + "description": "Chaos engineering experiments for Testing. Part of Quality Assurance documentation and deliverables." + }, + "ci-cd-pipeline-definitions": { + "file_pattern": "*.ci-cd-pipeline-definitions.md", + "content_type": "text/markdown", + "description": "CI/CD pipeline definitions for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "circuit-breaker-configurations": { + "file_pattern": "*.circuit-breaker-configurations.md", + "content_type": "text/markdown", + "description": "Circuit breaker configurations for Implementation. Part of Development documentation and deliverables." + }, + "class-diagrams": { + "file_pattern": "*.class-diagrams.*", + "description": "Class diagrams for Design. Part of Design & UX documentation and deliverables." + }, + "cloud-cost-optimization-reports": { + "file_pattern": "*.cloud-cost-optimization-reports.md", + "content_type": "text/markdown", + "description": "Cloud cost optimization reports for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "cloud-landing-zone-design": { + "file_pattern": "*.cloud-landing-zone-design.md", + "content_type": "text/markdown", + "description": "Cloud landing zone design for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "cmp-configurations": { + "file_pattern": "*.cmp-configurations.md", + "content_type": "text/markdown", + "description": "CMP configurations for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "code-coverage-reports": { + "file_pattern": "*.code-coverage-reports.md", + "content_type": "text/markdown", + "description": "Code coverage reports for Implementation. Part of Development documentation and deliverables." + }, + "code-review-records": { + "file_pattern": "*.code-review-records.md", + "content_type": "text/markdown", + "description": "Code review records for Implementation. Part of Development documentation and deliverables." + }, + "code-signing-records": { + "file_pattern": "*.code-signing-records.md", + "content_type": "text/markdown", + "description": "Code signing records for Mobile, Desktop, and Distribution. Part of Client Distribution documentation and deliverables." + }, + "coding-standards-and-style-guides": { + "file_pattern": "*.coding-standards-and-style-guides.md", + "content_type": "text/markdown", + "description": "Coding standards and style guides for Implementation. Part of Development documentation and deliverables." + }, + "commit-logs": { + "file_pattern": "*.commit-logs.md", + "content_type": "text/markdown", + "description": "Commit logs for Implementation. Part of Development documentation and deliverables." + }, + "communication-plan": { + "file_pattern": "*.communication-plan.md", + "content_type": "text/markdown", + "description": "Communication plan for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "competitive-analysis": { + "file_pattern": "*.competitive-analysis.md", + "content_type": "text/markdown", + "description": "Competitive analysis for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "component-diagrams": { + "file_pattern": "*.component-diagrams.*", + "description": "Component diagrams for Design. Part of Design & UX documentation and deliverables." + }, + "component-model": { + "file_pattern": "*.component-model.md", + "content_type": "text/markdown", + "description": "Component model for Architecture. Part of Application and Integration documentation and deliverables." + }, + "configuration-design": { + "file_pattern": "*.configuration-design.md", + "content_type": "text/markdown", + "description": "Configuration design for Design. Part of Design & UX documentation and deliverables." + }, + "configuration-drift-reports": { + "file_pattern": "*.configuration-drift-reports.md", + "content_type": "text/markdown", + "description": "Configuration drift reports for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "consent-models": { + "file_pattern": "*.consent-models.md", + "content_type": "text/markdown", + "description": "Consent models for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "consent-receipts": { + "file_pattern": "*.consent-receipts.md", + "content_type": "text/markdown", + "description": "Consent receipts for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "content-strategy": { + "file_pattern": "*.content-strategy.md", + "content_type": "text/markdown", + "description": "Content strategy for Design. Part of Design & UX documentation and deliverables." + }, + "context-diagrams": { + "file_pattern": "*.context-diagrams.*", + "description": "Context diagrams for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "continuous-improvement-plan": { + "file_pattern": "*.continuous-improvement-plan.md", + "content_type": "text/markdown", + "description": "Continuous improvement plan for Closure and Archival. Part of Project Closure documentation and deliverables." + }, + "contributing-guide": { + "file_pattern": "CONTRIBUTING.md", + "content_type": "text/markdown", + "description": "CONTRIBUTING guide for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "contributor-license-agreements": { + "file_pattern": "*.contributor-license-agreements.md", + "content_type": "text/markdown", + "description": "Contributor License Agreements (CLAs) for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "control-test-evidence-packs": { + "file_pattern": "*.control-test-evidence-packs.md", + "content_type": "text/markdown", + "description": "Control test evidence packs for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "cookie-policy-inventory": { + "file_pattern": "*.cookie-policy-inventory.md", + "content_type": "text/markdown", + "description": "Cookie policy inventory for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "cookie-policy": { + "file_pattern": "*.cookie-policy.md", + "content_type": "text/markdown", + "description": "Cookie Policy for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "cosign-signatures": { + "file_pattern": "*.cosign-signatures.md", + "content_type": "text/markdown", + "description": "Cosign signatures for Implementation. Part of Development documentation and deliverables." + }, + "cost-anomaly-alerts": { + "file_pattern": "*.cost-anomaly-alerts.md", + "content_type": "text/markdown", + "description": "Cost anomaly alerts for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "cost-tagging-policy": { + "file_pattern": "*.cost-tagging-policy.md", + "content_type": "text/markdown", + "description": "Cost tagging policy for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "crash-reporting-taxonomy": { + "file_pattern": "*.crash-reporting-taxonomy.md", + "content_type": "text/markdown", + "description": "Crash reporting taxonomy for Mobile, Desktop, and Distribution. Part of Client Distribution documentation and deliverables." + }, + "crash-triage-playbooks": { + "file_pattern": "*.crash-triage-playbooks.md", + "content_type": "text/markdown", + "description": "Crash triage playbooks for Mobile, Desktop, and Distribution. Part of Client Distribution documentation and deliverables." + }, + "customer-communication-templates": { + "file_pattern": "*.customer-communication-templates.md", + "content_type": "text/markdown", + "description": "Customer communication templates for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "customer-data-return-procedures": { + "file_pattern": "*.customer-data-return-procedures.md", + "content_type": "text/markdown", + "description": "Customer data return procedures for Closure and Archival. Part of Project Closure documentation and deliverables." + }, + "customer-onboarding-plan": { + "file_pattern": "*.customer-onboarding-plan.md", + "content_type": "text/markdown", + "description": "Customer onboarding plan for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "cutover-checklist": { + "file_pattern": "*.cutover-checklist.md", + "content_type": "text/markdown", + "description": "Cutover checklist for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "dag-definitions": { + "file_pattern": "*.dag-definitions.md", + "content_type": "text/markdown", + "description": "DAG definitions for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "data-contracts": { + "file_pattern": "*.data-contracts.md", + "content_type": "text/markdown", + "description": "Data contracts for Architecture. Part of Application and Integration documentation and deliverables." + }, + "data-dictionaries": { + "file_pattern": "*.data-dictionaries.md", + "content_type": "text/markdown", + "description": "Data dictionaries for Architecture. Part of Data and Information documentation and deliverables." + }, + "data-export-procedures": { + "file_pattern": "*.data-export-procedures.md", + "content_type": "text/markdown", + "description": "Data export procedures for Closure and Archival. Part of Project Closure documentation and deliverables." + }, + "data-flow-diagrams": { + "file_pattern": "*.data-flow-diagrams.*", + "description": "Data flow diagrams (DFDs) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "data-freshness-slas": { + "file_pattern": "*.data-freshness-slas.md", + "content_type": "text/markdown", + "description": "Data freshness SLAs for Architecture. Part of Data and Information documentation and deliverables." + }, + "data-lineage-maps": { + "file_pattern": "*.data-lineage-maps.*", + "description": "Data lineage maps for Architecture. Part of Data and Information documentation and deliverables." + }, + "data-lineage-tracking": { + "file_pattern": "*.data-lineage-tracking.md", + "content_type": "text/markdown", + "description": "Data lineage tracking for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "data-map": { + "file_pattern": "*.data-map.*", + "description": "Data map for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "data-processing-addendum": { + "file_pattern": "*.data-processing-addendum.md", + "content_type": "text/markdown", + "description": "Data Processing Addendum (DPA) for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "data-product-specification": { + "file_pattern": "*.data-product-specification.md", + "content_type": "text/markdown", + "description": "Data product specification for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "data-protection-impact-assessment": { + "file_pattern": "*.data-protection-impact-assessment.md", + "content_type": "text/markdown", + "description": "Data Protection Impact Assessment (DPIA) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "data-quality-rules": { + "file_pattern": "*.data-quality-rules.md", + "content_type": "text/markdown", + "description": "Data quality rules for Architecture. Part of Data and Information documentation and deliverables." + }, + "data-residency-plan": { + "file_pattern": "*.data-residency-plan.md", + "content_type": "text/markdown", + "description": "Data residency plan for Architecture. Part of Data and Information documentation and deliverables." + }, + "data-retention-plan": { + "file_pattern": "*.data-retention-plan.md", + "content_type": "text/markdown", + "description": "Data retention plan for Architecture. Part of Data and Information documentation and deliverables." + }, + "database-schema-ddl": { + "file_pattern": "*.database-schema-ddl.txt", + "content_type": "text/plain", + "description": "Database schema DDL for Architecture. Part of Data and Information documentation and deliverables." + }, + "dataset-documentation": { + "file_pattern": "*.dataset-documentation.md", + "content_type": "text/markdown", + "description": "Dataset documentation for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "ddos-posture-assessments": { + "file_pattern": "*.ddos-posture-assessments.md", + "content_type": "text/markdown", + "description": "DDoS posture assessments for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "decision-log": { + "file_pattern": "*.decision-log.md", + "content_type": "text/markdown", + "description": "Decision log for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "decommissioning-plan": { + "file_pattern": "*.decommissioning-plan.md", + "content_type": "text/markdown", + "description": "Decommissioning plan for Closure and Archival. Part of Project Closure documentation and deliverables." + }, + "defect-log": { + "file_pattern": "*.defect-log.md", + "content_type": "text/markdown", + "description": "Defect log for Testing. Part of Quality Assurance documentation and deliverables." + }, + "demo-scripts": { + "file_pattern": "*.demo-scripts.txt", + "content_type": "text/plain", + "description": "Demo scripts for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "dependency-graph": { + "file_pattern": "*.dependency-graph.*", + "description": "Dependency graph for Implementation. Part of Development documentation and deliverables." + }, + "deployment-diagram": { + "file_pattern": "*.deployment-diagram.*", + "description": "Deployment diagram for Design. Part of Design & UX documentation and deliverables." + }, + "deployment-plan": { + "file_pattern": "*.deployment-plan.md", + "content_type": "text/markdown", + "description": "Deployment plan (blue-green, canary, rolling) for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "deployment-topology-diagram": { + "file_pattern": "*.deployment-topology-diagram.*", + "description": "Deployment topology diagram for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "deprecation-policy": { + "file_pattern": "*.deprecation-policy.md", + "content_type": "text/markdown", + "description": "Deprecation policy for Architecture. Part of Application and Integration documentation and deliverables." + }, + "developer-handbook": { + "file_pattern": "*.developer-handbook.md", + "content_type": "text/markdown", + "description": "Developer handbook for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "disaster-recovery-runbooks": { + "file_pattern": "*.disaster-recovery-runbooks.md", + "content_type": "text/markdown", + "description": "Disaster recovery runbooks for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "discount-guardrails": { + "file_pattern": "*.discount-guardrails.md", + "content_type": "text/markdown", + "description": "Discount guardrails for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "dns-configurations": { + "file_pattern": "*.dns-configurations.md", + "content_type": "text/markdown", + "description": "DNS configurations for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "docker-compose-manifests": { + "schema": "schemas/docker-compose-manifests.json", + "file_pattern": "*.docker-compose-manifests.yaml", + "content_type": "application/yaml", + "description": "Docker Compose manifests for Implementation. Part of Development documentation and deliverables." + }, + "dockerfiles": { + "file_pattern": "*.dockerfiles.txt", + "content_type": "text/plain", + "description": "Dockerfiles for Implementation. Part of Development documentation and deliverables." + }, + "domain-model": { + "file_pattern": "*.domain-model.md", + "content_type": "text/markdown", + "description": "Domain model for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "dr-test-reports": { + "file_pattern": "*.dr-test-reports.md", + "content_type": "text/markdown", + "description": "DR test reports for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "drift-detection-reports": { + "file_pattern": "*.drift-detection-reports.md", + "content_type": "text/markdown", + "description": "Drift detection reports for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "dsar-playbooks": { + "file_pattern": "*.dsar-playbooks.md", + "content_type": "text/markdown", + "description": "DSAR playbooks for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "eccn-classification": { + "file_pattern": "*.eccn-classification.md", + "content_type": "text/markdown", + "description": "ECCN classification for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "encryption-and-key-management-design": { + "file_pattern": "*.encryption-and-key-management-design.md", + "content_type": "text/markdown", + "description": "Encryption and key management design for Architecture. Part of Security Architecture documentation and deliverables." + }, + "engagement-plan": { + "file_pattern": "*.engagement-plan.md", + "content_type": "text/markdown", + "description": "Engagement plan for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "enterprise-data-model": { + "file_pattern": "*.enterprise-data-model.md", + "content_type": "text/markdown", + "description": "Enterprise data model for Architecture. Part of Data and Information documentation and deliverables." + }, + "enterprise-risk-register": { + "file_pattern": "*.enterprise-risk-register.md", + "content_type": "text/markdown", + "description": "Enterprise risk register for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "environment-matrix": { + "file_pattern": "*.environment-matrix.md", + "content_type": "text/markdown", + "description": "Environment matrix for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "environment-promotion-rules": { + "file_pattern": "*.environment-promotion-rules.md", + "content_type": "text/markdown", + "description": "Environment promotion rules for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "epic-charter": { + "file_pattern": "*.epic-charter.md", + "content_type": "text/markdown", + "description": "Epic charter for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "er-diagrams": { + "file_pattern": "*.er-diagrams.*", + "description": "ER diagrams for Architecture. Part of Data and Information documentation and deliverables." + }, + "error-budget-policy": { + "file_pattern": "*.error-budget-policy.md", + "content_type": "text/markdown", + "description": "Error budget policy for Design. Part of Design & UX documentation and deliverables." + }, + "error-taxonomy": { + "file_pattern": "*.error-taxonomy.md", + "content_type": "text/markdown", + "description": "Error taxonomy for Architecture. Part of Application and Integration documentation and deliverables." + }, + "escalation-matrix": { + "file_pattern": "*.escalation-matrix.md", + "content_type": "text/markdown", + "description": "Escalation matrix for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "etl-elt-specifications": { + "file_pattern": "*.etl-elt-specifications.md", + "content_type": "text/markdown", + "description": "ETL/ELT specifications for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "evaluation-protocols": { + "file_pattern": "*.evaluation-protocols.txt", + "content_type": "text/plain", + "description": "Evaluation protocols for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "event-schemas": { + "schema": "schemas/event-schemas.json", + "file_pattern": "*.event-schemas.json", + "content_type": "application/json", + "description": "Event schemas (Avro, JSON, Protobuf) for Architecture. Part of Application and Integration documentation and deliverables." + }, + "eviction-policies": { + "file_pattern": "*.eviction-policies.md", + "content_type": "text/markdown", + "description": "Eviction policies for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "exception-log": { + "file_pattern": "*.exception-log.md", + "content_type": "text/markdown", + "description": "Exception log for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "exception-register": { + "file_pattern": "*.exception-register.md", + "content_type": "text/markdown", + "description": "Exception register for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "experiment-tracking-logs": { + "file_pattern": "*.experiment-tracking-logs.md", + "content_type": "text/markdown", + "description": "Experiment tracking logs for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "explainability-reports": { + "file_pattern": "*.explainability-reports.md", + "content_type": "text/markdown", + "description": "Explainability reports (SHAP, LIME) for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "export-control-screening": { + "file_pattern": "*.export-control-screening.md", + "content_type": "text/markdown", + "description": "Export control screening for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "faq": { + "file_pattern": "*.faq.md", + "content_type": "text/markdown", + "description": "FAQ for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "feasibility-study": { + "file_pattern": "*.feasibility-study.md", + "content_type": "text/markdown", + "description": "Feasibility study for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "feature-flag-registry": { + "file_pattern": "*.feature-flag-registry.md", + "content_type": "text/markdown", + "description": "Feature flag registry for Implementation. Part of Development documentation and deliverables." + }, + "feature-rollback-playbooks": { + "file_pattern": "*.feature-rollback-playbooks.md", + "content_type": "text/markdown", + "description": "Feature rollback playbooks for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "feature-store-contracts": { + "file_pattern": "*.feature-store-contracts.md", + "content_type": "text/markdown", + "description": "Feature store contracts for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "finops-dashboards": { + "file_pattern": "*.finops-dashboards.md", + "content_type": "text/markdown", + "description": "FinOps dashboards for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "firewall-rules": { + "file_pattern": "*.firewall-rules.md", + "content_type": "text/markdown", + "description": "Firewall rules for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "functional-requirements-specification": { + "file_pattern": "*.functional-requirements-specification.md", + "content_type": "text/markdown", + "description": "Functional Requirements Specification (FRS) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "genai-safety-evaluations": { + "file_pattern": "*.genai-safety-evaluations.md", + "content_type": "text/markdown", + "description": "GenAI safety evaluations for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "glossary-and-taxonomy-index": { + "file_pattern": "*.glossary-and-taxonomy-index.md", + "content_type": "text/markdown", + "description": "Glossary and taxonomy index for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "go-no-go-minutes": { + "file_pattern": "*.go-no-go-minutes.md", + "content_type": "text/markdown", + "description": "Go/no-go minutes for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "golden-path-guide": { + "file_pattern": "*.golden-path-guide.md", + "content_type": "text/markdown", + "description": "Golden path guide for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "governance-charter": { + "file_pattern": "*.governance-charter.md", + "content_type": "text/markdown", + "description": "Governance charter for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "graphql-schema": { + "file_pattern": "*.graphql-schema.*", + "description": "GraphQL schema for Architecture. Part of Application and Integration documentation and deliverables." + }, + "great-expectations-suites": { + "file_pattern": "*.great-expectations-suites.md", + "content_type": "text/markdown", + "description": "Great Expectations suites for Architecture. Part of Data and Information documentation and deliverables." + }, + "grpc-proto-files": { + "file_pattern": "*.grpc-proto-files.txt", + "content_type": "text/plain", + "description": "gRPC proto files for Architecture. Part of Application and Integration documentation and deliverables." + }, + "gtm-checklist": { + "file_pattern": "*.gtm-checklist.md", + "content_type": "text/markdown", + "description": "GTM checklist for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "helm-charts": { + "schema": "schemas/helm-charts.json", + "file_pattern": "*/Chart.yaml", + "content_type": "application/yaml", + "description": "Helm charts for Implementation. Part of Development documentation and deliverables." + }, + "high-fidelity-mockups": { + "file_pattern": "*.high-fidelity-mockups.*", + "description": "High-fidelity mockups for Design. Part of Design & UX documentation and deliverables." + }, + "hsm-procedures": { + "file_pattern": "*.hsm-procedures.md", + "content_type": "text/markdown", + "description": "HSM procedures for Architecture. Part of Security Architecture documentation and deliverables." + }, + "hyperparameter-configurations": { + "file_pattern": "*.hyperparameter-configurations.md", + "content_type": "text/markdown", + "description": "Hyperparameter configurations for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "iac-module-registry": { + "file_pattern": "*.iac-module-registry.md", + "content_type": "text/markdown", + "description": "IaC module registry for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "iam-design": { + "file_pattern": "*.iam-design.md", + "content_type": "text/markdown", + "description": "IAM design for Architecture. Part of Security Architecture documentation and deliverables." + }, + "idempotency-and-replay-protection-policy": { + "file_pattern": "*.idempotency-and-replay-protection-policy.md", + "content_type": "text/markdown", + "description": "Idempotency and replay protection policy for Architecture. Part of Application and Integration documentation and deliverables." + }, + "incident-management-plan": { + "file_pattern": "*.incident-management-plan.md", + "content_type": "text/markdown", + "description": "Incident management plan for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "incident-reports": { + "file_pattern": "*.incident-reports.md", + "content_type": "text/markdown", + "description": "Incident reports for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "information-architecture": { + "file_pattern": "*.information-architecture.md", + "content_type": "text/markdown", + "description": "Information architecture for Design. Part of Design & UX documentation and deliverables." + }, + "initiative-charter": { + "file_pattern": "*.initiative-charter.md", + "content_type": "text/markdown", + "description": "Initiative charter for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "installation-guides": { + "file_pattern": "*.installation-guides.md", + "content_type": "text/markdown", + "description": "Installation guides for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "installer-manifests": { + "file_pattern": "*.installer-manifests.md", + "content_type": "text/markdown", + "description": "Installer manifests for Mobile, Desktop, and Distribution. Part of Client Distribution documentation and deliverables." + }, + "interactive-prototypes": { + "file_pattern": "*.interactive-prototypes.*", + "description": "Interactive prototypes for Design. Part of Design & UX documentation and deliverables." + }, + "interface-control-document": { + "file_pattern": "*.interface-control-document.md", + "content_type": "text/markdown", + "description": "Interface Control Document (ICD) for Architecture. Part of Application and Integration documentation and deliverables." + }, + "ip-register": { + "file_pattern": "*.ip-register.md", + "content_type": "text/markdown", + "description": "IP register for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "iso-27001-mapping": { + "file_pattern": "*.iso-27001-mapping.*", + "description": "ISO 27001 mapping for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "joiner-mover-leaver-workflows": { + "file_pattern": "*.joiner-mover-leaver-workflows.md", + "content_type": "text/markdown", + "description": "Joiner-Mover-Leaver workflows for HR, Access, and Lifecycle. Part of Access & Identity documentation and deliverables." + }, + "key-ceremony-records": { + "file_pattern": "*.key-ceremony-records.md", + "content_type": "text/markdown", + "description": "Key ceremony records for Architecture. Part of Security Architecture documentation and deliverables." + }, + "kill-switch-designs": { + "file_pattern": "*.kill-switch-designs.md", + "content_type": "text/markdown", + "description": "Kill-switch designs for Implementation. Part of Development documentation and deliverables." + }, + "knowledge-base-articles": { + "file_pattern": "*.knowledge-base-articles.md", + "content_type": "text/markdown", + "description": "Knowledge base articles for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "kpi-framework": { + "file_pattern": "*.kpi-framework.md", + "content_type": "text/markdown", + "description": "KPI framework for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "kustomize-manifests": { + "schema": "schemas/kustomize-manifests.json", + "file_pattern": "*.kustomize-manifests.yaml", + "content_type": "application/yaml", + "description": "Kustomize manifests for Implementation. Part of Development documentation and deliverables." + }, + "labs-and-workshops": { + "file_pattern": "*.labs-and-workshops.md", + "content_type": "text/markdown", + "description": "Labs and workshops for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "legal-hold-procedures": { + "file_pattern": "*.legal-hold-procedures.md", + "content_type": "text/markdown", + "description": "Legal hold procedures for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "lessons-learned-document": { + "file_pattern": "*.lessons-learned-document.md", + "content_type": "text/markdown", + "description": "Lessons learned document for Closure and Archival. Part of Project Closure documentation and deliverables." + }, + "load-balancer-configurations": { + "file_pattern": "*.load-balancer-configurations.md", + "content_type": "text/markdown", + "description": "Load balancer configurations for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "load-profiles": { + "file_pattern": "*.load-profiles.md", + "content_type": "text/markdown", + "description": "Load profiles for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "load-test-report": { + "file_pattern": "*.load-test-report.md", + "content_type": "text/markdown", + "description": "Load test report for Testing. Part of Quality Assurance documentation and deliverables." + }, + "locale-files": { + "file_pattern": "*.locale-files.md", + "content_type": "text/markdown", + "description": "Locale files for Design. Part of Design & UX documentation and deliverables." + }, + "localization-plan": { + "file_pattern": "*.localization-plan.md", + "content_type": "text/markdown", + "description": "Localization plan for Design. Part of Design & UX documentation and deliverables." + }, + "logging-taxonomy": { + "file_pattern": "*.logging-taxonomy.md", + "content_type": "text/markdown", + "description": "Logging taxonomy for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "logical-architecture-diagram": { + "file_pattern": "*.logical-architecture-diagram.*", + "description": "Logical architecture diagram for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "logical-data-model": { + "file_pattern": "*.logical-data-model.md", + "content_type": "text/markdown", + "description": "Logical data model for Architecture. Part of Data and Information documentation and deliverables." + }, + "market-analysis": { + "file_pattern": "*.market-analysis.md", + "content_type": "text/markdown", + "description": "Market analysis for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "messaging-frameworks": { + "file_pattern": "*.messaging-frameworks.md", + "content_type": "text/markdown", + "description": "Messaging frameworks for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "metadata-catalogs": { + "file_pattern": "*.metadata-catalogs.md", + "content_type": "text/markdown", + "description": "Metadata catalogs for Architecture. Part of Data and Information documentation and deliverables." + }, + "metric-catalog": { + "file_pattern": "*.metric-catalog.md", + "content_type": "text/markdown", + "description": "Metric catalog for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "microcopy-guides": { + "file_pattern": "*.microcopy-guides.md", + "content_type": "text/markdown", + "description": "Microcopy guides for Design. Part of Design & UX documentation and deliverables." + }, + "migration-scripts": { + "file_pattern": "*.migration-scripts.txt", + "content_type": "text/plain", + "description": "Migration scripts (Liquibase/Flyway) for Architecture. Part of Data and Information documentation and deliverables." + }, + "mission-statement": { + "file_pattern": "*.mission-statement.md", + "content_type": "text/markdown", + "description": "Mission statement for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "model-cards": { + "file_pattern": "*.model-cards.md", + "content_type": "text/markdown", + "description": "Model cards for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "model-governance-policy": { + "file_pattern": "*.model-governance-policy.md", + "content_type": "text/markdown", + "description": "Model governance policy for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "model-registry-entries": { + "file_pattern": "*.model-registry-entries.md", + "content_type": "text/markdown", + "description": "Model registry entries for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "model-risk-assessments": { + "file_pattern": "*.model-risk-assessments.md", + "content_type": "text/markdown", + "description": "Model risk assessments for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "monitoring-and-observability-design": { + "file_pattern": "*.monitoring-and-observability-design.md", + "content_type": "text/markdown", + "description": "Monitoring and observability design for Design. Part of Design & UX documentation and deliverables." + }, + "monitoring-dashboards": { + "file_pattern": "*.monitoring-dashboards.md", + "content_type": "text/markdown", + "description": "Monitoring dashboards for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "multi-region-active-active-plan": { + "file_pattern": "*.multi-region-active-active-plan.md", + "content_type": "text/markdown", + "description": "Multi-region active-active plan for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "network-policies": { + "file_pattern": "*.network-policies.md", + "content_type": "text/markdown", + "description": "Network policies for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "network-topology-diagram": { + "file_pattern": "*.network-topology-diagram.*", + "description": "Network topology diagram for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "non-functional-requirements-matrix": { + "file_pattern": "*.non-functional-requirements-matrix.md", + "content_type": "text/markdown", + "description": "Non-Functional Requirements (NFR) matrix for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "notarization-records": { + "file_pattern": "*.notarization-records.md", + "content_type": "text/markdown", + "description": "Notarization records for Mobile, Desktop, and Distribution. Part of Client Distribution documentation and deliverables." + }, + "offboarding-checklist": { + "file_pattern": "*.offboarding-checklist.md", + "content_type": "text/markdown", + "description": "Offboarding checklist for HR, Access, and Lifecycle. Part of Access & Identity documentation and deliverables." + }, + "okr-definitions": { + "file_pattern": "*.okr-definitions.md", + "content_type": "text/markdown", + "description": "OKR definitions for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "on-call-handbook": { + "file_pattern": "*.on-call-handbook.md", + "content_type": "text/markdown", + "description": "On-call handbook for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "onboarding-checklist": { + "file_pattern": "*.onboarding-checklist.md", + "content_type": "text/markdown", + "description": "Onboarding checklist for HR, Access, and Lifecycle. Part of Access & Identity documentation and deliverables." + }, + "onboarding-guide": { + "file_pattern": "*.onboarding-guide.md", + "content_type": "text/markdown", + "description": "Onboarding guide for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "open-source-license-bom": { + "file_pattern": "*.open-source-license-bom.md", + "content_type": "text/markdown", + "description": "Open source license BoM for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "openapi-specification": { + "file_pattern": "*.openapi-specification.md", + "content_type": "text/markdown", + "description": "OpenAPI specification for Architecture. Part of Application and Integration documentation and deliverables." + }, + "operational-acceptance-certificate": { + "file_pattern": "*.operational-acceptance-certificate.md", + "content_type": "text/markdown", + "description": "Operational acceptance certificate for Closure and Archival. Part of Project Closure documentation and deliverables." + }, + "operations-manual": { + "file_pattern": "*.operations-manual.md", + "content_type": "text/markdown", + "description": "Operations manual for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "ownership-charters": { + "file_pattern": "*.ownership-charters.md", + "content_type": "text/markdown", + "description": "Ownership charters for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "patterns-and-anti-patterns-library": { + "file_pattern": "*.patterns-and-anti-patterns-library.md", + "content_type": "text/markdown", + "description": "Patterns and anti-patterns library for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "penetration-testing-report": { + "file_pattern": "*.penetration-testing-report.md", + "content_type": "text/markdown", + "description": "Penetration testing report for Testing. Part of Quality Assurance documentation and deliverables." + }, + "performance-strategy": { + "file_pattern": "*.performance-strategy.md", + "content_type": "text/markdown", + "description": "Performance strategy for Design. Part of Design & UX documentation and deliverables." + }, + "performance-test-plan": { + "file_pattern": "*.performance-test-plan.md", + "content_type": "text/markdown", + "description": "Performance test plan for Testing. Part of Quality Assurance documentation and deliverables." + }, + "performance-test-results": { + "file_pattern": "*.performance-test-results.md", + "content_type": "text/markdown", + "description": "Performance test results for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "physical-architecture-diagram": { + "file_pattern": "*.physical-architecture-diagram.*", + "description": "Physical architecture diagram for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "physical-data-model": { + "file_pattern": "*.physical-data-model.md", + "content_type": "text/markdown", + "description": "Physical data model for Architecture. Part of Data and Information documentation and deliverables." + }, + "pipeline-architecture-diagram": { + "file_pattern": "*.pipeline-architecture-diagram.*", + "description": "Pipeline architecture diagram for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "pipeline-definitions": { + "file_pattern": "*.pipeline-definitions.md", + "content_type": "text/markdown", + "description": "Pipeline definitions for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "platform-services-catalog": { + "file_pattern": "*.platform-services-catalog.md", + "content_type": "text/markdown", + "description": "Platform services catalog for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "playbooks": { + "file_pattern": "*.playbooks.md", + "content_type": "text/markdown", + "description": "Playbooks for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "portfolio-roadmap": { + "file_pattern": "*.portfolio-roadmap.*", + "description": "Portfolio roadmap for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "positioning-documents": { + "file_pattern": "*.positioning-documents.md", + "content_type": "text/markdown", + "description": "Positioning documents for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "post-implementation-review": { + "file_pattern": "*.post-implementation-review.md", + "content_type": "text/markdown", + "description": "Post-implementation review for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "post-mortem-report": { + "file_pattern": "*.post-mortem-report.md", + "content_type": "text/markdown", + "description": "Post-mortem report for Closure and Archival. Part of Project Closure documentation and deliverables." + }, + "price-books": { + "file_pattern": "*.price-books.md", + "content_type": "text/markdown", + "description": "Price books for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "pricing-and-packaging-strategy": { + "file_pattern": "*.pricing-and-packaging-strategy.md", + "content_type": "text/markdown", + "description": "Pricing and packaging strategy for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "privacy-impact-assessment": { + "file_pattern": "*.privacy-impact-assessment.md", + "content_type": "text/markdown", + "description": "Privacy Impact Assessment (PIA) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "privacy-labels": { + "file_pattern": "*.privacy-labels.md", + "content_type": "text/markdown", + "description": "Privacy labels for Mobile, Desktop, and Distribution. Part of Client Distribution documentation and deliverables." + }, + "privacy-policy": { + "file_pattern": "*.privacy-policy.md", + "content_type": "text/markdown", + "description": "Privacy Policy for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "product-launch-plan": { + "file_pattern": "*.product-launch-plan.md", + "content_type": "text/markdown", + "description": "Product launch plan for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "product-requirements-document": { + "file_pattern": "*.product-requirements-document.md", + "content_type": "text/markdown", + "description": "Product Requirements Document (PRD) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "product-strategy": { + "file_pattern": "*.product-strategy.md", + "content_type": "text/markdown", + "description": "Product strategy for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "production-hygiene-checklist": { + "file_pattern": "*.production-hygiene-checklist.md", + "content_type": "text/markdown", + "description": "Production hygiene checklist for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "program-increment-plan": { + "file_pattern": "*.program-increment-plan.md", + "content_type": "text/markdown", + "description": "Program increment plan for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "promotion-rules": { + "file_pattern": "*.promotion-rules.md", + "content_type": "text/markdown", + "description": "Promotion rules for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "promotion-workflows": { + "file_pattern": "*.promotion-workflows.md", + "content_type": "text/markdown", + "description": "Promotion workflows for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "prompt-engineering-policy": { + "file_pattern": "*.prompt-engineering-policy.md", + "content_type": "text/markdown", + "description": "Prompt engineering policy for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "provenance-attestations": { + "file_pattern": "*.provenance-attestations.md", + "content_type": "text/markdown", + "description": "Provenance attestations (in-toto) for Implementation. Part of Development documentation and deliverables." + }, + "provenance-chain-documentation": { + "file_pattern": "*.provenance-chain-documentation.md", + "content_type": "text/markdown", + "description": "Provenance chain documentation (SLSA) for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "pseudo-localization-reports": { + "file_pattern": "*.pseudo-localization-reports.md", + "content_type": "text/markdown", + "description": "Pseudo-localization reports for Design. Part of Design & UX documentation and deliverables." + }, + "pull-request-summaries": { + "file_pattern": "*.pull-request-summaries.md", + "content_type": "text/markdown", + "description": "Pull request summaries for Implementation. Part of Development documentation and deliverables." + }, + "purple-team-reports": { + "file_pattern": "*.purple-team-reports.md", + "content_type": "text/markdown", + "description": "Purple team reports for Architecture. Part of Security Architecture documentation and deliverables." + }, + "qbr-templates": { + "file_pattern": "*.qbr-templates.md", + "content_type": "text/markdown", + "description": "QBR templates for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "quarterly-access-reviews": { + "file_pattern": "*.quarterly-access-reviews.md", + "content_type": "text/markdown", + "description": "Quarterly access reviews for HR, Access, and Lifecycle. Part of Access & Identity documentation and deliverables." + }, + "raci-per-workstream": { + "file_pattern": "*.raci-per-workstream.md", + "content_type": "text/markdown", + "description": "RACI per workstream for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "raid-log": { + "file_pattern": "*.raid-log.md", + "content_type": "text/markdown", + "description": "RAID log for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "rate-limiting-policy": { + "file_pattern": "*.rate-limiting-policy.md", + "content_type": "text/markdown", + "description": "Rate limiting policy for Architecture. Part of Application and Integration documentation and deliverables." + }, + "rbac-abac-matrix": { + "file_pattern": "*.rbac-abac-matrix.md", + "content_type": "text/markdown", + "description": "RBAC/ABAC matrix for Architecture. Part of Security Architecture documentation and deliverables." + }, + "rbac-abac-policy": { + "file_pattern": "*.rbac-abac-policy.md", + "content_type": "text/markdown", + "description": "RBAC/ABAC policy for HR, Access, and Lifecycle. Part of Access & Identity documentation and deliverables." + }, + "readme": { + "file_pattern": "README.md", + "content_type": "text/markdown", + "description": "README for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "records-of-processing-activities": { + "file_pattern": "*.records-of-processing-activities.md", + "content_type": "text/markdown", + "description": "Records of Processing Activities (RoPA) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "red-team-reports": { + "file_pattern": "*.red-team-reports.md", + "content_type": "text/markdown", + "description": "Red team reports for Architecture. Part of Security Architecture documentation and deliverables." + }, + "red-teaming-reports": { + "file_pattern": "*.red-teaming-reports.md", + "content_type": "text/markdown", + "description": "Red-teaming reports for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "reference-architectures": { + "file_pattern": "*.reference-architectures.md", + "content_type": "text/markdown", + "description": "Reference architectures for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "regression-test-suite": { + "file_pattern": "*.regression-test-suite.md", + "content_type": "text/markdown", + "description": "Regression test suite for Testing. Part of Quality Assurance documentation and deliverables." + }, + "regulatory-mapping": { + "file_pattern": "*.regulatory-mapping.*", + "description": "Regulatory mapping (SOC2, ISO, NIST, HIPAA, PCI, FedRAMP) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "release-certification": { + "file_pattern": "*.release-certification.md", + "content_type": "text/markdown", + "description": "Release certification for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "release-notes": { + "file_pattern": "*.release-notes.md", + "content_type": "text/markdown", + "description": "Release notes for Implementation. Part of Development documentation and deliverables." + }, + "release-plan": { + "file_pattern": "*.release-plan.md", + "content_type": "text/markdown", + "description": "Release plan for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "release-risk-assessment": { + "file_pattern": "*.release-risk-assessment.md", + "content_type": "text/markdown", + "description": "Release risk assessment for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "remediation-tracker": { + "file_pattern": "*.remediation-tracker.md", + "content_type": "text/markdown", + "description": "Remediation tracker for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "renewal-playbooks": { + "file_pattern": "*.renewal-playbooks.md", + "content_type": "text/markdown", + "description": "Renewal playbooks for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "reproducibility-checklists": { + "file_pattern": "*.reproducibility-checklists.md", + "content_type": "text/markdown", + "description": "Reproducibility checklists for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "requirements-traceability-matrix": { + "file_pattern": "*.requirements-traceability-matrix.md", + "content_type": "text/markdown", + "description": "Requirements traceability matrix for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "resource-plan": { + "file_pattern": "*.resource-plan.md", + "content_type": "text/markdown", + "description": "Resource plan for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "retention-schedule": { + "file_pattern": "*.retention-schedule.md", + "content_type": "text/markdown", + "description": "Retention schedule for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "reverse-etl-playbooks": { + "file_pattern": "*.reverse-etl-playbooks.md", + "content_type": "text/markdown", + "description": "Reverse ETL playbooks for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "risk-appetite-statement": { + "file_pattern": "*.risk-appetite-statement.md", + "content_type": "text/markdown", + "description": "Risk appetite statement for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "roi-model": { + "file_pattern": "*.roi-model.md", + "content_type": "text/markdown", + "description": "ROI model for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "roi-tco-calculators": { + "file_pattern": "*.roi-tco-calculators.md", + "content_type": "text/markdown", + "description": "ROI/TCO calculators for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "role-catalog": { + "file_pattern": "*.role-catalog.md", + "content_type": "text/markdown", + "description": "Role catalog for HR, Access, and Lifecycle. Part of Access & Identity documentation and deliverables." + }, + "rollback-plan": { + "file_pattern": "*.rollback-plan.md", + "content_type": "text/markdown", + "description": "Rollback plan for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "root-cause-analyses": { + "file_pattern": "*.root-cause-analyses.md", + "content_type": "text/markdown", + "description": "Root cause analyses (RCA) for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "runbooks": { + "file_pattern": "*.runbooks.md", + "content_type": "text/markdown", + "description": "Runbooks for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "safety-filter-configurations": { + "file_pattern": "*.safety-filter-configurations.md", + "content_type": "text/markdown", + "description": "Safety filter configurations for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "sales-enablement-kits": { + "file_pattern": "*.sales-enablement-kits.md", + "content_type": "text/markdown", + "description": "Sales enablement kits for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "sbom-policy": { + "schema": "schemas/sbom-policy.json", + "file_pattern": "*.sbom-policy.json", + "content_type": "application/json", + "description": "SBOM policy for CI/CD, Build, and Provenance. Part of Build & Release Automation documentation and deliverables." + }, + "sbom-verification-reports": { + "schema": "schemas/sbom-verification-reports.json", + "file_pattern": "*.sbom-verification-reports.json", + "content_type": "application/json", + "description": "SBOM verification reports for Deployment and Release. Part of Release Management documentation and deliverables." + }, + "scaling-policies": { + "file_pattern": "*.scaling-policies.md", + "content_type": "text/markdown", + "description": "Scaling policies for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "scheduling-slas": { + "file_pattern": "*.scheduling-slas.md", + "content_type": "text/markdown", + "description": "Scheduling SLAs for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "schema-evolution-policy": { + "file_pattern": "*.schema-evolution-policy.md", + "content_type": "text/markdown", + "description": "Schema evolution policy for Architecture. Part of Application and Integration documentation and deliverables." + }, + "secret-rotation-schedule": { + "file_pattern": "*.secret-rotation-schedule.md", + "content_type": "text/markdown", + "description": "Secret rotation schedule for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "secrets-management-policy": { + "file_pattern": "*.secrets-management-policy.md", + "content_type": "text/markdown", + "description": "Secrets management policy for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "secure-coding-checklist": { + "file_pattern": "*.secure-coding-checklist.md", + "content_type": "text/markdown", + "description": "Secure coding checklist for Implementation. Part of Development documentation and deliverables." + }, + "secure-coding-policy": { + "file_pattern": "*.secure-coding-policy.md", + "content_type": "text/markdown", + "description": "Secure coding policy for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "security-architecture-diagram": { + "file_pattern": "*.security-architecture-diagram.*", + "description": "Security architecture diagram for Architecture. Part of Security Architecture documentation and deliverables." + }, + "security-detections-catalog": { + "file_pattern": "*.security-detections-catalog.md", + "content_type": "text/markdown", + "description": "Security detections catalog (MITRE ATT&CK) for Architecture. Part of Security Architecture documentation and deliverables." + }, + "security-policy-library": { + "file_pattern": "*.security-policy-library.md", + "content_type": "text/markdown", + "description": "Security policy library for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "security-test-results": { + "file_pattern": "*.security-test-results.md", + "content_type": "text/markdown", + "description": "Security test results (SAST, DAST, IAST) for Testing. Part of Quality Assurance documentation and deliverables." + }, + "semantic-layer-definitions": { + "file_pattern": "*.semantic-layer-definitions.md", + "content_type": "text/markdown", + "description": "Semantic layer definitions (dbt, LookML) for Architecture. Part of Data and Information documentation and deliverables." + }, + "sequence-diagrams": { + "file_pattern": "*.sequence-diagrams.*", + "description": "Sequence diagrams for Design. Part of Design & UX documentation and deliverables." + }, + "service-configuration-files": { + "file_pattern": "*.service-configuration-files.md", + "content_type": "text/markdown", + "description": "Service configuration files for Implementation. Part of Development documentation and deliverables." + }, + "service-decomposition": { + "file_pattern": "*.service-decomposition.md", + "content_type": "text/markdown", + "description": "Service decomposition for Architecture. Part of Application and Integration documentation and deliverables." + }, + "service-dependency-graph": { + "file_pattern": "*.service-dependency-graph.*", + "description": "Service dependency graph for Architecture. Part of Application and Integration documentation and deliverables." + }, + "service-level-objectives": { + "file_pattern": "*.service-level-objectives.md", + "content_type": "text/markdown", + "description": "Service-level objectives (SLOs) for Design. Part of Design & UX documentation and deliverables." + }, + "service-mesh-configurations": { + "file_pattern": "*.service-mesh-configurations.md", + "content_type": "text/markdown", + "description": "Service mesh configurations for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "shadow-canary-deployment-scorecards": { + "file_pattern": "*.shadow-canary-deployment-scorecards.md", + "content_type": "text/markdown", + "description": "Shadow/canary deployment scorecards for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "showback-and-chargeback-reports": { + "file_pattern": "*.showback-and-chargeback-reports.md", + "content_type": "text/markdown", + "description": "Showback and chargeback reports for Infrastructure and Platform Engineering. Part of Platform Engineering documentation and deliverables." + }, + "sig-questionnaires": { + "file_pattern": "*.sig-questionnaires.md", + "content_type": "text/markdown", + "description": "SIG questionnaires for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "skills-matrix": { + "file_pattern": "*.skills-matrix.md", + "content_type": "text/markdown", + "description": "Skills matrix for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "sla-slo-schedules": { + "file_pattern": "*.sla-slo-schedules.md", + "content_type": "text/markdown", + "description": "SLA/SLO schedules for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "soc-2-control-implementation-matrix": { + "file_pattern": "*.soc-2-control-implementation-matrix.md", + "content_type": "text/markdown", + "description": "SOC 2 control implementation matrix for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "sod-conflict-matrices": { + "file_pattern": "*.sod-conflict-matrices.md", + "content_type": "text/markdown", + "description": "SoD conflict matrices for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "sod-matrix": { + "file_pattern": "*.sod-matrix.md", + "content_type": "text/markdown", + "description": "SoD matrix for Architecture. Part of Security Architecture documentation and deliverables." + }, + "software-bill-of-materials": { + "schema": "schemas/software-bill-of-materials.json", + "file_pattern": "*.software-bill-of-materials.json", + "content_type": "application/json", + "description": "Software Bill of Materials (SBOM) for Implementation. Part of Development documentation and deliverables." + }, + "solution-briefs": { + "file_pattern": "*.solution-briefs.md", + "content_type": "text/markdown", + "description": "Solution briefs for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "source-code-repositories": { + "file_pattern": "*.source-code-repositories.md", + "content_type": "text/markdown", + "description": "Source code repositories for Implementation. Part of Development documentation and deliverables." + }, + "sprint-goals": { + "file_pattern": "*.sprint-goals.md", + "content_type": "text/markdown", + "description": "Sprint goals for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "staffing-plan": { + "file_pattern": "*.staffing-plan.md", + "content_type": "text/markdown", + "description": "Staffing plan for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "stakeholder-map": { + "file_pattern": "*.stakeholder-map.*", + "description": "Stakeholder map for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "standard-contractual-clauses": { + "file_pattern": "*.standard-contractual-clauses.md", + "content_type": "text/markdown", + "description": "Standard Contractual Clauses (SCCs) for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "standard-operating-procedures": { + "file_pattern": "*.standard-operating-procedures.md", + "content_type": "text/markdown", + "description": "Standard operating procedures (SOPs) for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "state-diagrams": { + "file_pattern": "*.state-diagrams.*", + "description": "State diagrams for Design. Part of Design & UX documentation and deliverables." + }, + "static-analysis-reports": { + "file_pattern": "*.static-analysis-reports.md", + "content_type": "text/markdown", + "description": "Static analysis reports for Implementation. Part of Development documentation and deliverables." + }, + "status-page-communication-templates": { + "file_pattern": "*.status-page-communication-templates.md", + "content_type": "text/markdown", + "description": "Status page communication templates for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "steering-committee-minutes": { + "file_pattern": "*.steering-committee-minutes.md", + "content_type": "text/markdown", + "description": "Steering committee minutes for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "storyboards": { + "file_pattern": "*.storyboards.*", + "description": "Storyboards for Design. Part of Design & UX documentation and deliverables." + }, + "subprocessor-notifications": { + "file_pattern": "*.subprocessor-notifications.md", + "content_type": "text/markdown", + "description": "Subprocessor notifications for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "success-plan-templates": { + "file_pattern": "*.success-plan-templates.md", + "content_type": "text/markdown", + "description": "Success plan templates for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "sustainability-reports": { + "file_pattern": "*.sustainability-reports.md", + "content_type": "text/markdown", + "description": "Sustainability reports for Performance, Capacity, and Cost. Part of Performance & Optimization documentation and deliverables." + }, + "sync-contracts": { + "file_pattern": "*.sync-contracts.md", + "content_type": "text/markdown", + "description": "Sync contracts for Data Engineering and Analytics. Part of Data Platform documentation and deliverables." + }, + "synthetic-data-generation-plan": { + "file_pattern": "*.synthetic-data-generation-plan.md", + "content_type": "text/markdown", + "description": "Synthetic data generation plan for Testing. Part of Quality Assurance documentation and deliverables." + }, + "system-requirements-specification": { + "file_pattern": "*.system-requirements-specification.md", + "content_type": "text/markdown", + "description": "System Requirements Specification (SRS) for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "target-state-evolution-map": { + "file_pattern": "*.target-state-evolution-map.*", + "description": "Target-state evolution map for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "team-topology-map": { + "file_pattern": "*.team-topology-map.*", + "description": "Team topology map for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "technology-standards-catalog": { + "file_pattern": "*.technology-standards-catalog.md", + "content_type": "text/markdown", + "description": "Technology standards catalog for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "telemetry-schema": { + "file_pattern": "*.telemetry-schema.txt", + "content_type": "text/plain", + "description": "Telemetry schema for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "tenancy-and-isolation-model": { + "file_pattern": "*.tenancy-and-isolation-model.md", + "content_type": "text/markdown", + "description": "Tenancy and isolation model for Architecture. Part of High-Level and Platform documentation and deliverables." + }, + "terms-of-service": { + "file_pattern": "*.terms-of-service.md", + "content_type": "text/markdown", + "description": "Terms of Service for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "test-case-specifications": { + "file_pattern": "*.test-case-specifications.md", + "content_type": "text/markdown", + "description": "Test case specifications for Testing. Part of Quality Assurance documentation and deliverables." + }, + "test-data-specification": { + "file_pattern": "*.test-data-specification.md", + "content_type": "text/markdown", + "description": "Test data specification for Testing. Part of Quality Assurance documentation and deliverables." + }, + "test-plan": { + "file_pattern": "*.test-plan.md", + "content_type": "text/markdown", + "description": "Test plan for Testing. Part of Quality Assurance documentation and deliverables." + }, + "test-strategy": { + "file_pattern": "*.test-strategy.md", + "content_type": "text/markdown", + "description": "Test strategy for Testing. Part of Quality Assurance documentation and deliverables." + }, + "third-party-risk-assessments": { + "file_pattern": "*.third-party-risk-assessments.md", + "content_type": "text/markdown", + "description": "Third-party risk assessments for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "threat-model": { + "file_pattern": "*.threat-model.md", + "content_type": "text/markdown", + "description": "Threat model (STRIDE, attack trees) for Architecture. Part of Security Architecture documentation and deliverables." + }, + "time-allocation-worksheets": { + "file_pattern": "*.time-allocation-worksheets.md", + "content_type": "text/markdown", + "description": "Time allocation worksheets for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "toil-reduction-plan": { + "file_pattern": "*.toil-reduction-plan.md", + "content_type": "text/markdown", + "description": "Toil reduction plan for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "topic-and-queue-catalog": { + "file_pattern": "*.topic-and-queue-catalog.md", + "content_type": "text/markdown", + "description": "Topic and queue catalog for Architecture. Part of Application and Integration documentation and deliverables." + }, + "traceability-matrix": { + "file_pattern": "*.traceability-matrix.md", + "content_type": "text/markdown", + "description": "Traceability matrix for Testing. Part of Quality Assurance documentation and deliverables." + }, + "trademark-guidance": { + "file_pattern": "*.trademark-guidance.md", + "content_type": "text/markdown", + "description": "Trademark guidance for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "training-curriculum": { + "file_pattern": "*.training-curriculum.md", + "content_type": "text/markdown", + "description": "Training curriculum for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "training-data-cards": { + "file_pattern": "*.training-data-cards.md", + "content_type": "text/markdown", + "description": "Training data cards for AI/ML and Model Ops. Part of Model Development & Governance documentation and deliverables." + }, + "triage-rules": { + "file_pattern": "*.triage-rules.md", + "content_type": "text/markdown", + "description": "Triage rules for Testing. Part of Quality Assurance documentation and deliverables." + }, + "troubleshooting-trees": { + "file_pattern": "*.troubleshooting-trees.md", + "content_type": "text/markdown", + "description": "Troubleshooting trees for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "trust-center-content-plan": { + "file_pattern": "*.trust-center-content-plan.md", + "content_type": "text/markdown", + "description": "Trust center content plan for Product Management and GTM. Part of Product & Market documentation and deliverables." + }, + "trust-center-evidence-summaries": { + "file_pattern": "*.trust-center-evidence-summaries.md", + "content_type": "text/markdown", + "description": "Trust center evidence summaries for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "uat-plan": { + "file_pattern": "*.uat-plan.md", + "content_type": "text/markdown", + "description": "UAT plan for Testing. Part of Quality Assurance documentation and deliverables." + }, + "uat-sign-off-document": { + "file_pattern": "*.uat-sign-off-document.md", + "content_type": "text/markdown", + "description": "UAT sign-off document for Testing. Part of Quality Assurance documentation and deliverables." + }, + "upgrade-guides": { + "file_pattern": "*.upgrade-guides.md", + "content_type": "text/markdown", + "description": "Upgrade guides for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "uptime-methodology": { + "file_pattern": "*.uptime-methodology.md", + "content_type": "text/markdown", + "description": "Uptime methodology for Public-Facing and Legal. Part of Legal & External documentation and deliverables." + }, + "use-case-diagrams": { + "file_pattern": "*.use-case-diagrams.*", + "description": "Use-case diagrams for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "use-case-models": { + "file_pattern": "*.use-case-models.md", + "content_type": "text/markdown", + "description": "Use-case models for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "user-journeys": { + "file_pattern": "*.user-journeys.md", + "content_type": "text/markdown", + "description": "User journeys for Design. Part of Design & UX documentation and deliverables." + }, + "user-manuals": { + "file_pattern": "*.user-manuals.md", + "content_type": "text/markdown", + "description": "User manuals for Documentation, Support, and Training. Part of Knowledge & Enablement documentation and deliverables." + }, + "user-stories": { + "file_pattern": "*.user-stories.md", + "content_type": "text/markdown", + "description": "User stories for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "velocity-and-burndown-reports": { + "file_pattern": "*.velocity-and-burndown-reports.md", + "content_type": "text/markdown", + "description": "Velocity and burndown reports for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "vendor-management-pack": { + "file_pattern": "*.vendor-management-pack.md", + "content_type": "text/markdown", + "description": "Vendor management pack for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "vendor-scorecards": { + "file_pattern": "*.vendor-scorecards.md", + "content_type": "text/markdown", + "description": "Vendor scorecards for Portfolio, Governance, and Delivery Ops. Part of Governance & Planning documentation and deliverables." + }, + "version-tags": { + "file_pattern": "*.version-tags.md", + "content_type": "text/markdown", + "description": "Version tags for Implementation. Part of Development documentation and deliverables." + }, + "vision-statement": { + "file_pattern": "*.vision-statement.md", + "content_type": "text/markdown", + "description": "Vision statement for Inception / Strategy. Part of Business & Strategy documentation and deliverables." + }, + "vpat-acr-results": { + "file_pattern": "*.vpat-acr-results.md", + "content_type": "text/markdown", + "description": "VPAT/ACR results for Requirements and Analysis. Part of Requirements & Analysis documentation and deliverables." + }, + "vulnerability-disclosure-policy": { + "file_pattern": "*.vulnerability-disclosure-policy.md", + "content_type": "text/markdown", + "description": "Vulnerability disclosure policy for Security, Privacy, Audit, and Compliance. Part of Governance, Risk & Compliance documentation and deliverables." + }, + "vulnerability-management-plan": { + "file_pattern": "*.vulnerability-management-plan.md", + "content_type": "text/markdown", + "description": "Vulnerability management plan for Operations, SRE, and Maintenance. Part of Operations & Reliability documentation and deliverables." + }, + "wireframes": { + "file_pattern": "*.wireframes.*", + "description": "Wireframes for Design. Part of Design & UX documentation and deliverables." + }, + "zero-trust-design": { + "file_pattern": "*.zero-trust-design.md", + "content_type": "text/markdown", + "description": "Zero trust design for Architecture. Part of Security Architecture documentation and deliverables." + }, + +} + + +def get_artifact_definition(artifact_type: str) -> Optional[Dict[str, Any]]: + """ + Get the definition for a known artifact type. + + Args: + artifact_type: Artifact type identifier + + Returns: + Artifact definition dictionary with schema, file_pattern, etc., or None if unknown + """ + if artifact_type in KNOWN_ARTIFACT_TYPES: + definition = {"type": artifact_type} + definition.update(KNOWN_ARTIFACT_TYPES[artifact_type]) + return definition + return None + + +def validate_artifact_type(artifact_type: str) -> tuple[bool, Optional[str]]: + """ + Validate that an artifact type is known or suggest registering it. + + Args: + artifact_type: Artifact type identifier + + Returns: + Tuple of (is_valid, warning_message) + """ + if artifact_type in KNOWN_ARTIFACT_TYPES: + return True, None + + warning = f"Artifact type '{artifact_type}' is not in the known registry. " + warning += "Consider documenting it in docs/ARTIFACT_STANDARDS.md and creating a schema." + return False, warning + + +def generate_artifact_metadata( + skill_name: str, + produces: Optional[List[str]] = None, + consumes: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Generate artifact metadata structure. + + Args: + skill_name: Name of the skill + produces: List of artifact types produced + consumes: List of artifact types consumed + + Returns: + Artifact metadata dictionary + """ + metadata = {} + warnings = [] + + # Build produces section + if produces: + produces_list = [] + for artifact_type in produces: + is_known, warning = validate_artifact_type(artifact_type) + if warning: + warnings.append(warning) + + artifact_def = {"type": artifact_type} + + # Add known metadata if available + if artifact_type in KNOWN_ARTIFACT_TYPES: + known = KNOWN_ARTIFACT_TYPES[artifact_type] + if "schema" in known: + artifact_def["schema"] = known["schema"] + if "file_pattern" in known: + artifact_def["file_pattern"] = known["file_pattern"] + if "content_type" in known: + artifact_def["content_type"] = known["content_type"] + if "description" in known: + artifact_def["description"] = known["description"] + + produces_list.append(artifact_def) + + metadata["produces"] = produces_list + + # Build consumes section + if consumes: + consumes_list = [] + for artifact_type in consumes: + is_known, warning = validate_artifact_type(artifact_type) + if warning: + warnings.append(warning) + + artifact_def = { + "type": artifact_type, + "required": True # Default to required + } + + # Add description if known + if artifact_type in KNOWN_ARTIFACT_TYPES: + known = KNOWN_ARTIFACT_TYPES[artifact_type] + if "description" in known: + artifact_def["description"] = known["description"] + + consumes_list.append(artifact_def) + + metadata["consumes"] = consumes_list + + return metadata, warnings + + +def format_as_yaml(metadata: Dict[str, Any]) -> str: + """ + Format artifact metadata as YAML for inclusion in skill.yaml. + + Args: + metadata: Artifact metadata dictionary + + Returns: + Formatted YAML string + """ + yaml_str = "artifact_metadata:\n" + yaml_str += yaml.dump(metadata, default_flow_style=False, indent=2, sort_keys=False) + return yaml_str + + +def main(): + """CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Define artifact metadata for Betty Framework skills" + ) + parser.add_argument( + "skill_name", + help="Name of the skill (e.g., api.define)" + ) + parser.add_argument( + "--produces", + nargs="+", + help="Artifact types this skill produces" + ) + parser.add_argument( + "--consumes", + nargs="+", + help="Artifact types this skill consumes" + ) + parser.add_argument( + "--output-file", + default="artifact_metadata.yaml", + help="Output file path" + ) + + args = parser.parse_args() + + logger.info(f"Generating artifact metadata for skill: {args.skill_name}") + + try: + # Generate metadata + metadata, warnings = generate_artifact_metadata( + args.skill_name, + produces=args.produces, + consumes=args.consumes + ) + + # Format as YAML + yaml_content = format_as_yaml(metadata) + + # Save to file + output_path = args.output_file + with open(output_path, 'w') as f: + f.write(yaml_content) + + logger.info(f"✅ Generated artifact metadata: {output_path}") + + # Print to stdout + print("\n# Add this to your skill.yaml:\n") + print(yaml_content) + + # Show warnings + if warnings: + logger.warning("\n⚠️ Warnings:") + for warning in warnings: + logger.warning(f" - {warning}") + + # Print summary + logger.info("\n📋 Summary:") + if metadata.get("produces"): + logger.info(f" Produces: {', '.join(a['type'] for a in metadata['produces'])}") + if metadata.get("consumes"): + logger.info(f" Consumes: {', '.join(a['type'] for a in metadata['consumes'])}") + + # Success result + result = { + "ok": True, + "status": "success", + "skill_name": args.skill_name, + "metadata": metadata, + "output_file": output_path, + "warnings": warnings + } + + print("\n" + json.dumps(result, indent=2)) + sys.exit(0) + + except Exception as e: + logger.error(f"Failed to generate artifact metadata: {e}") + result = { + "ok": False, + "status": "failed", + "error": str(e) + } + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/artifact.define/registry_loader.py b/skills/artifact.define/registry_loader.py new file mode 100644 index 0000000..06770d2 --- /dev/null +++ b/skills/artifact.define/registry_loader.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Artifact Registry Loader - Load artifact types from JSON + +This module provides the single source of truth for artifact types. +The registry is loaded from registry/artifact_types.json at runtime. +""" + +import json +from pathlib import Path +from typing import Dict, Any +from functools import lru_cache + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +@lru_cache(maxsize=1) +def load_artifact_registry() -> Dict[str, Dict[str, Any]]: + """ + Load artifact types from JSON registry file. + + Returns: + Dictionary mapping artifact type names to their metadata + + Raises: + FileNotFoundError: If registry file doesn't exist + json.JSONDecodeError: If registry file is invalid JSON + """ + registry_file = Path(BASE_DIR) / "registry" / "artifact_types.json" + + if not registry_file.exists(): + logger.error(f"Registry file not found: {registry_file}") + raise FileNotFoundError(f"Artifact registry not found at {registry_file}") + + try: + with open(registry_file, 'r') as f: + data = json.load(f) + + # Convert list format to dictionary format + registry = {} + for artifact in data.get('artifact_types', []): + name = artifact.get('name') + if not name: + continue + + # Build metadata dictionary (exclude name since it's the key) + metadata = {} + if artifact.get('description'): + metadata['description'] = artifact['description'] + if artifact.get('file_pattern'): + metadata['file_pattern'] = artifact['file_pattern'] + if artifact.get('content_type'): + metadata['content_type'] = artifact['content_type'] + if artifact.get('schema'): + metadata['schema'] = artifact['schema'] + + registry[name] = metadata + + logger.info(f"Loaded {len(registry)} artifact types from registry") + return registry + + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in registry file: {e}") + raise + except Exception as e: + logger.error(f"Error loading registry: {e}") + raise + + +# Load the registry on module import +try: + KNOWN_ARTIFACT_TYPES = load_artifact_registry() +except Exception as e: + logger.warning(f"Failed to load artifact registry: {e}") + logger.warning("Using empty registry as fallback") + KNOWN_ARTIFACT_TYPES = {} + + +def reload_registry(): + """ + Reload the artifact registry from disk. + + This clears the cache and forces a fresh load from the JSON file. + Useful for development and testing. + """ + load_artifact_registry.cache_clear() + global KNOWN_ARTIFACT_TYPES + KNOWN_ARTIFACT_TYPES = load_artifact_registry() + logger.info("Registry reloaded") + return KNOWN_ARTIFACT_TYPES + + +def get_artifact_count() -> int: + """Get the number of registered artifact types""" + return len(KNOWN_ARTIFACT_TYPES) + + +def is_registered(artifact_type: str) -> bool: + """Check if an artifact type is registered""" + return artifact_type in KNOWN_ARTIFACT_TYPES + + +def get_artifact_metadata(artifact_type: str) -> Dict[str, Any]: + """ + Get metadata for a specific artifact type. + + Args: + artifact_type: The artifact type identifier + + Returns: + Dictionary with artifact metadata, or empty dict if not found + """ + return KNOWN_ARTIFACT_TYPES.get(artifact_type, {}) diff --git a/skills/artifact.define/skill.yaml b/skills/artifact.define/skill.yaml new file mode 100644 index 0000000..8b1ddf5 --- /dev/null +++ b/skills/artifact.define/skill.yaml @@ -0,0 +1,93 @@ +name: artifact.define +version: 0.1.0 +description: > + Define artifact metadata for Betty Framework skills. Helps create artifact_metadata + blocks that declare what artifacts a skill produces and consumes, enabling + skill interoperability and autonomous agent composition. + +inputs: + - name: skill_name + type: string + required: true + description: Name of the skill to define artifact metadata for + + - name: produces + type: array + required: false + description: List of artifact types this skill produces (e.g., openapi-spec, validation-report) + + - name: consumes + type: array + required: false + description: List of artifact types this skill consumes + + - name: output_file + type: string + required: false + default: artifact_metadata.yaml + description: Where to save the generated artifact metadata + +outputs: + - name: artifact_metadata + type: object + description: Generated artifact metadata block + + - name: metadata_file + type: string + description: Path to saved artifact metadata file + + - name: validation_result + type: object + description: Validation results for the artifact metadata + +dependencies: + - context.schema + +entrypoints: + - command: /skill/artifact/define + handler: artifact_define.py + runtime: python + description: > + Create artifact metadata for a skill. Validates artifact types against + known schemas, suggests file patterns, and generates properly formatted + artifact_metadata blocks for skill.yaml files. + parameters: + - name: skill_name + type: string + required: true + description: Name of the skill (e.g., api.define, workflow.validate) + - name: produces + type: array + required: false + description: Artifact types produced (e.g., ["openapi-spec", "validation-report"]) + - name: consumes + type: array + required: false + description: Artifact types consumed + - name: output_file + type: string + required: false + default: artifact_metadata.yaml + description: Output file path + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: + - artifacts + - metadata + - scaffolding + - interoperability + - layer3 + +# This skill's own artifact metadata! +artifact_metadata: + produces: + - type: artifact-metadata-definition + description: Artifact metadata YAML block for skill.yaml files + file_pattern: "artifact_metadata.yaml" + content_type: application/yaml + + consumes: [] # Doesn't consume artifacts, creates from user input diff --git a/skills/artifact.review/README.md b/skills/artifact.review/README.md new file mode 100644 index 0000000..c25aca1 --- /dev/null +++ b/skills/artifact.review/README.md @@ -0,0 +1,465 @@ +# artifact.review + +AI-powered artifact content review for quality, completeness, and best practices compliance. + +## Purpose + +The `artifact.review` skill provides intelligent content quality assessment to ensure: +- Complete and substantive content +- Professional writing quality +- Best practices adherence +- Industry standards alignment +- Readiness for approval/publication + +## Features + +✅ **Content Analysis**: Depth, completeness, placeholder detection +✅ **Professional Quality**: Tone, structure, clarity assessment +✅ **Best Practices**: Versioning, governance, traceability checks +✅ **Industry Standards**: Framework and compliance alignment +✅ **Readiness Scoring**: 0-100 publication readiness score +✅ **Quality Rating**: Excellent, Good, Fair, Needs Improvement, Poor +✅ **Smart Recommendations**: Prioritized, actionable feedback +✅ **Multiple Review Levels**: Quick, standard, comprehensive + +## Usage + +### Basic Review + +```bash +python3 skills/artifact.review/artifact_review.py +``` + +### With Artifact Type + +```bash +python3 skills/artifact.review/artifact_review.py \ + my-artifact.yaml \ + --artifact-type business-case +``` + +### Review Level + +```bash +python3 skills/artifact.review/artifact_review.py \ + my-artifact.yaml \ + --review-level comprehensive +``` + +**Review Levels**: +- `quick` - Basic checks (future: < 1 second) +- `standard` - Comprehensive review (default) +- `comprehensive` - Deep analysis (future enhancement) + +### Save Review Report + +```bash +python3 skills/artifact.review/artifact_review.py \ + my-artifact.yaml \ + --output review-report.yaml +``` + +## Review Dimensions + +### 1. Content Completeness (35% weight) + +**Analyzes**: +- Word count and content depth +- Placeholder content (TODO, TBD, etc.) +- Field population percentage +- Section completeness + +**Scoring Factors**: +- Content too brief (< 100 words): Major issue +- Limited depth (< 300 words): Issue +- Good depth (300+ words): Strength +- Many placeholders (> 10): Major issue +- Few placeholders (< 5): Recommendation +- No placeholders: Strength +- Content fields populated: Percentage-based score + +**Example Feedback**: +``` +Content Completeness: 33/100 + Word Count: 321 + Placeholders: 21 + ✅ Good content depth (321 words) + ❌ Many placeholders found (21) - content is incomplete +``` + +### 2. Professional Quality (25% weight) + +**Analyzes**: +- Executive summary presence +- Clear document structure +- Professional tone and language +- Passive voice usage +- Business jargon overuse + +**Checks For**: +- ❌ Informal contractions (gonna, wanna) +- ❌ Internet slang (lol, omg) +- ❌ Excessive exclamation marks +- ❌ Multiple question marks +- 🟡 Excessive passive voice (> 50% of sentences) +- 🟡 Jargon overload (> 3 instances) + +**Example Feedback**: +``` +Professional Quality: 100/100 + ✅ Includes executive summary/overview + ✅ Clear document structure +``` + +### 3. Best Practices (25% weight) + +**Analyzes**: +- Semantic versioning (1.0.0 format) +- Document classification standards +- Approval workflow definition +- Change history maintenance +- Related document links + +**Artifact-Specific Checks**: +- **business-case**: ROI analysis, financial justification +- **threat-model**: STRIDE methodology, threat frameworks +- **test-plan**: Test criteria (pass/fail conditions) + +**Example Feedback**: +``` +Best Practices: 100/100 + ✅ Uses semantic versioning + ✅ Proper document classification set + ✅ Approval workflow defined + ✅ Maintains change history + ✅ Links to related documents +``` + +### 4. Industry Standards (15% weight) + +**Detects References To**: +- TOGAF - Architecture framework +- ISO 27001 - Information security +- NIST - Cybersecurity framework +- PCI-DSS - Payment card security +- GDPR - Data privacy +- SOC 2 - Service organization controls +- HIPAA - Healthcare privacy +- SAFe - Scaled agile framework +- ITIL - IT service management +- COBIT - IT governance +- PMBOK - Project management +- OWASP - Application security + +**Recommendations Based On Type**: +- Security artifacts → ISO 27001, NIST, OWASP +- Architecture → TOGAF, Zachman +- Governance → COBIT, PMBOK +- Compliance → SOC 2, GDPR, HIPAA + +**Example Feedback**: +``` +Industry Standards: 100/100 + ✅ References: PCI-DSS, ISO 27001 + ✅ References industry standards: PCI-DSS, ISO 27001 +``` + +## Readiness Score Calculation + +``` +Readiness Score = + (Completeness × 0.35) + + (Professional Quality × 0.25) + + (Best Practices × 0.25) + + (Industry Standards × 0.15) +``` + +## Quality Ratings + +| Score | Rating | Meaning | Recommendation | +|-------|--------|---------|----------------| +| 90-100 | Excellent | Ready for publication | Submit for approval | +| 75-89 | Good | Ready for approval | Minor polish recommended | +| 60-74 | Fair | Needs refinement | Address key recommendations | +| 40-59 | Needs Improvement | Significant gaps | Major content work needed | +| < 40 | Poor | Major revision required | Substantial rework needed | + +## Review Report Structure + +```yaml +success: true +review_results: + artifact_path: /path/to/artifact.yaml + artifact_type: business-case + file_format: yaml + review_level: standard + reviewed_at: 2025-10-25T19:30:00 + + completeness: + score: 33 + word_count: 321 + placeholder_count: 21 + issues: + - "Many placeholders found (21) - content is incomplete" + strengths: + - "Good content depth (321 words)" + recommendations: + - "Replace 21 placeholder(s) with actual content" + + professional_quality: + score: 100 + issues: [] + strengths: + - "Includes executive summary/overview" + - "Clear document structure" + recommendations: [] + + best_practices: + score: 100 + issues: [] + strengths: + - "Uses semantic versioning" + - "Proper document classification set" + recommendations: [] + + industry_standards: + score: 100 + referenced_standards: + - "PCI-DSS" + - "ISO 27001" + strengths: + - "References industry standards: PCI-DSS, ISO 27001" + recommendations: [] + +readiness_score: 72 +quality_rating: "Fair" +summary_recommendations: + - "🔴 CRITICAL: Many placeholders found (21)" + - "🟡 Add ROI/financial justification" +strengths: + - "Good content depth (321 words)" + - "Includes executive summary/overview" + # ... more strengths +``` + +## Recommendations System + +### Recommendation Priorities + +**🔴 CRITICAL**: Issues that must be fixed +- Incomplete content sections +- Many placeholders (> 10) +- Missing required analysis + +**🟡 RECOMMENDED**: Improvements that should be made +- Few placeholders (< 10) +- Missing best practice elements +- Industry standard gaps + +**🟢 OPTIONAL**: Nice-to-have enhancements +- Minor polish suggestions +- Additional context recommendations + +### Top 10 Recommendations + +The review returns the top 10 most important recommendations, prioritized by: +1. Critical issues first +2. Standard recommendations +3. Most impactful improvements + +## Usage Examples + +### Example 1: Review Business Case + +```bash +$ python3 skills/artifact.review/artifact_review.py \ + artifacts/customer-portal-business-case.yaml + +====================================================================== +Artifact Content Review Report +====================================================================== +Artifact: artifacts/customer-portal-business-case.yaml +Type: business-case +Review Level: standard + +Quality Rating: Fair +Readiness Score: 66/100 + +Content Completeness: 18/100 + Word Count: 312 + Placeholders: 16 + ✅ Good content depth (312 words) + ❌ Many placeholders found (16) - content is incomplete + +Professional Quality: 100/100 + ✅ Includes executive summary/overview + ✅ Clear document structure + +Best Practices: 100/100 + ✅ Uses semantic versioning + ✅ Approval workflow defined + +Industry Standards: 70/100 + +Top Recommendations: + 🔴 CRITICAL: Many placeholders found (16) + 🟡 Add ROI/financial justification + +Overall Assessment: + 🟡 Fair quality - needs refinement before approval +====================================================================== +``` + +### Example 2: Comprehensive Review + +```bash +$ python3 skills/artifact.review/artifact_review.py \ + artifacts/threat-model.yaml \ + --review-level comprehensive \ + --output threat-model-review.yaml + +# Review saved to threat-model-review.yaml +# Use for audit trail and tracking improvements +``` + +## Integration with artifact.validate + +**Recommended workflow**: + +```bash +# 1. Validate structure first +python3 skills/artifact.validate/artifact_validate.py my-artifact.yaml --strict + +# 2. If valid, review content quality +if [ $? -eq 0 ]; then + python3 skills/artifact.review/artifact_review.py my-artifact.yaml +fi +``` + +**Combined quality gate**: + +```bash +# Both validation and review must pass +python3 skills/artifact.validate/artifact_validate.py my-artifact.yaml --strict && \ +python3 skills/artifact.review/artifact_review.py my-artifact.yaml | grep -q "Excellent\|Good" +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: Artifact Quality Review + +on: [pull_request] + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Review artifact quality + run: | + score=$(python3 skills/artifact.review/artifact_review.py \ + artifacts/my-artifact.yaml | \ + grep "Readiness Score:" | \ + awk '{print $3}' | \ + cut -d'/' -f1) + + if [ $score -lt 75 ]; then + echo "❌ Quality score too low: $score/100" + exit 1 + fi + + echo "✅ Quality score acceptable: $score/100" +``` + +### Quality Gates + +```bash +#!/bin/bash +# quality-gate.sh + +ARTIFACT=$1 +MIN_SCORE=${2:-75} + +score=$(python3 skills/artifact.review/artifact_review.py "$ARTIFACT" | \ + grep "Readiness Score:" | awk '{print $3}' | cut -d'/' -f1) + +if [ $score -ge $MIN_SCORE ]; then + echo "✅ PASSED: Quality score $score >= $MIN_SCORE" + exit 0 +else + echo "❌ FAILED: Quality score $score < $MIN_SCORE" + exit 1 +fi +``` + +## Command-Line Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `artifact_path` | string | required | Path to artifact file | +| `--artifact-type` | string | auto-detect | Artifact type override | +| `--review-level` | string | standard | quick, standard, comprehensive | +| `--output` | string | none | Save report to file | + +## Exit Codes + +- `0`: Review completed successfully +- `1`: Review failed (file not found, format error) + +Note: Exit code does NOT reflect quality score. Use output parsing for quality gates. + +## Performance + +- **Review time**: < 1 second per artifact +- **Memory usage**: < 15MB +- **Scalability**: Can review 1000+ artifacts in batch + +## Artifact Type Intelligence + +The review adapts recommendations based on artifact type: + +| Artifact Type | Special Checks | +|---------------|----------------| +| business-case | ROI analysis, financial justification | +| threat-model | STRIDE methodology, attack vectors | +| test-plan | Pass/fail criteria, test coverage | +| architecture-* | Framework references, design patterns | +| *-policy | Enforcement mechanisms, compliance | + +## Dependencies + +- Python 3.7+ +- `yaml` (PyYAML) - YAML parsing +- `artifact.define` skill - Artifact registry +- `artifact_descriptions/` - Best practices reference (optional) + +## Status + +**Active** - Phase 2 implementation complete + +## Tags + +artifacts, review, quality, ai-powered, best-practices, tier2, phase2 + +## Version History + +- **0.1.0** (2025-10-25): Initial implementation + - Content completeness analysis + - Professional quality assessment + - Best practices compliance + - Industry standards detection + - Readiness scoring + - Quality ratings + - Actionable recommendations + +## See Also + +- `artifact.validate` - Structure and schema validation +- `artifact.create` - Generate artifacts from templates +- `artifact_descriptions/` - Best practices guides +- `docs/ARTIFACT_USAGE_GUIDE.md` - Complete usage guide +- `PHASE2_COMPLETE.md` - Phase 2 overview diff --git a/skills/artifact.review/__init__.py b/skills/artifact.review/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/artifact.review/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/artifact.review/artifact_review.py b/skills/artifact.review/artifact_review.py new file mode 100755 index 0000000..abc3dda --- /dev/null +++ b/skills/artifact.review/artifact_review.py @@ -0,0 +1,628 @@ +#!/usr/bin/env python3 +""" +artifact.review skill - AI-powered artifact content review + +Reviews artifact quality, completeness, and best practices compliance. +Generates detailed assessments with actionable recommendations. +""" + +import sys +import os +import argparse +import re +from pathlib import Path +from datetime import datetime +from typing import Dict, Any, List, Optional, Tuple +import yaml + + +def load_artifact_registry() -> Dict[str, Any]: + """Load artifact registry from artifact.define skill""" + registry_file = Path(__file__).parent.parent / "artifact.define" / "artifact_define.py" + + if not registry_file.exists(): + raise FileNotFoundError(f"Artifact registry not found: {registry_file}") + + with open(registry_file, 'r') as f: + content = f.read() + + start_marker = "KNOWN_ARTIFACT_TYPES = {" + start_idx = content.find(start_marker) + if start_idx == -1: + raise ValueError("Could not find KNOWN_ARTIFACT_TYPES in registry file") + + start_idx += len(start_marker) - 1 + + brace_count = 0 + end_idx = start_idx + for i in range(start_idx, len(content)): + if content[i] == '{': + brace_count += 1 + elif content[i] == '}': + brace_count -= 1 + if brace_count == 0: + end_idx = i + 1 + break + + dict_str = content[start_idx:end_idx] + artifacts = eval(dict_str) + return artifacts + + +def detect_artifact_type(file_path: Path, content: str) -> Optional[str]: + """Detect artifact type from filename or content""" + filename = file_path.stem + registry = load_artifact_registry() + + if filename in registry: + return filename + + for artifact_type in registry.keys(): + if artifact_type in filename: + return artifact_type + + if file_path.suffix in ['.yaml', '.yml']: + try: + data = yaml.safe_load(content) + if isinstance(data, dict) and 'metadata' in data: + metadata = data['metadata'] + if 'artifactType' in metadata: + return metadata['artifactType'] + except: + pass + + return None + + +def load_artifact_description(artifact_type: str) -> Optional[str]: + """Load artifact description for reference""" + desc_dir = Path(__file__).parent.parent.parent / "artifact_descriptions" + desc_file = desc_dir / f"{artifact_type}.md" + + if desc_file.exists(): + with open(desc_file, 'r') as f: + return f.read() + return None + + +def analyze_content_completeness(content: str, data: Dict[str, Any], file_format: str) -> Dict[str, Any]: + """Analyze content completeness and depth""" + issues = [] + strengths = [] + recommendations = [] + + word_count = len(content.split()) + + # Check content depth + if word_count < 100: + issues.append("Very brief content - needs significant expansion") + recommendations.append("Add detailed explanations, examples, and context") + elif word_count < 300: + issues.append("Limited content depth - could be more comprehensive") + recommendations.append("Expand key sections with more details and examples") + else: + strengths.append(f"Good content depth ({word_count} words)") + + # Check for placeholder content + placeholder_patterns = [ + r'TODO', + r'Lorem ipsum', + r'placeholder', + r'REPLACE THIS', + r'FILL IN', + r'TBD', + r'coming soon' + ] + + placeholder_count = 0 + for pattern in placeholder_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + placeholder_count += len(matches) + + if placeholder_count > 10: + issues.append(f"Many placeholders found ({placeholder_count}) - content is incomplete") + elif placeholder_count > 5: + issues.append(f"Several placeholders found ({placeholder_count}) - needs completion") + elif placeholder_count > 0: + recommendations.append(f"Replace {placeholder_count} placeholder(s) with actual content") + else: + strengths.append("No placeholder text found") + + # YAML specific checks + if file_format in ['yaml', 'yml'] and isinstance(data, dict): + if 'content' in data: + content_section = data['content'] + if isinstance(content_section, dict): + filled_fields = [k for k, v in content_section.items() if v and str(v).strip() and 'TODO' not in str(v)] + total_fields = len(content_section) + completeness_pct = (len(filled_fields) / total_fields * 100) if total_fields > 0 else 0 + + if completeness_pct < 30: + issues.append(f"Content section is {completeness_pct:.0f}% complete - needs significant work") + elif completeness_pct < 70: + issues.append(f"Content section is {completeness_pct:.0f}% complete - needs more details") + elif completeness_pct < 100: + recommendations.append(f"Content section is {completeness_pct:.0f}% complete - finish remaining fields") + else: + strengths.append("Content section is fully populated") + + score = max(0, 100 - (len(issues) * 25) - (placeholder_count * 2)) + + return { + 'score': min(score, 100), + 'word_count': word_count, + 'placeholder_count': placeholder_count, + 'issues': issues, + 'strengths': strengths, + 'recommendations': recommendations + } + + +def analyze_professional_quality(content: str, file_format: str) -> Dict[str, Any]: + """Analyze professional writing quality and tone""" + issues = [] + strengths = [] + recommendations = [] + + # Check for professional tone indicators + has_executive_summary = 'executive summary' in content.lower() or 'overview' in content.lower() + has_clear_structure = bool(re.search(r'^#+\s+\w+', content, re.MULTILINE)) if file_format == 'md' else True + + if has_executive_summary: + strengths.append("Includes executive summary/overview") + else: + recommendations.append("Consider adding an executive summary for stakeholders") + + if has_clear_structure: + strengths.append("Clear document structure") + + # Check for unprofessional elements + informal_markers = [ + (r'\b(gonna|wanna|gotta)\b', 'informal contractions'), + (r'\b(lol|omg|wtf)\b', 'casual internet slang'), + (r'!!!+', 'excessive exclamation marks'), + (r'\?\?+', 'multiple question marks') + ] + + for pattern, issue_name in informal_markers: + if re.search(pattern, content, re.IGNORECASE): + issues.append(f"Contains {issue_name} - use professional language") + + # Check for passive voice (simplified check) + passive_patterns = r'\b(is|are|was|were|be|been|being)\s+\w+ed\b' + passive_count = len(re.findall(passive_patterns, content, re.IGNORECASE)) + total_sentences = len(re.findall(r'[.!?]', content)) + + if total_sentences > 0: + passive_ratio = passive_count / total_sentences + if passive_ratio > 0.5: + recommendations.append("Consider reducing passive voice for clearer communication") + + # Check for jargon overuse + jargon_markers = [ + 'synergy', 'leverage', 'paradigm shift', 'circle back', 'touch base', + 'low-hanging fruit', 'move the needle', 'boil the ocean' + ] + jargon_count = sum(1 for marker in jargon_markers if marker in content.lower()) + if jargon_count > 3: + recommendations.append("Reduce business jargon - use clear, specific language") + + score = max(0, 100 - (len(issues) * 20)) + + return { + 'score': score, + 'issues': issues, + 'strengths': strengths, + 'recommendations': recommendations + } + + +def check_best_practices(content: str, artifact_type: str, data: Dict[str, Any]) -> Dict[str, Any]: + """Check adherence to artifact-specific best practices""" + issues = [] + strengths = [] + recommendations = [] + + # Load artifact description for best practices reference + description = load_artifact_description(artifact_type) + + # Common best practices + if isinstance(data, dict): + # Metadata best practices + if 'metadata' in data: + metadata = data['metadata'] + + # Version control + if 'version' in metadata and metadata['version']: + if re.match(r'^\d+\.\d+\.\d+$', str(metadata['version'])): + strengths.append("Uses semantic versioning") + else: + recommendations.append("Consider using semantic versioning (e.g., 1.0.0)") + + # Classification + if 'classification' in metadata and metadata['classification']: + if metadata['classification'] in ['Public', 'Internal', 'Confidential', 'Restricted']: + strengths.append("Proper document classification set") + else: + issues.append("Invalid classification level") + + # Approval workflow + if 'approvers' in metadata and isinstance(metadata['approvers'], list): + if len(metadata['approvers']) > 0: + strengths.append("Approval workflow defined") + else: + recommendations.append("Add approvers to metadata for proper governance") + + # Change history best practice + if 'changeHistory' in data: + history = data['changeHistory'] + if isinstance(history, list) and len(history) > 0: + strengths.append("Maintains change history") + else: + recommendations.append("Document changes in change history") + + # Related documents + if 'relatedDocuments' in data or ('metadata' in data and 'relatedDocuments' in data['metadata']): + strengths.append("Links to related documents") + else: + recommendations.append("Link related artifacts for traceability") + + # Artifact-specific checks based on type + if artifact_type == 'business-case': + if 'roi' in content.lower() or 'return on investment' in content.lower(): + strengths.append("Includes ROI analysis") + else: + recommendations.append("Add ROI/financial justification") + + elif artifact_type == 'threat-model': + if 'stride' in content.lower() or 'attack vector' in content.lower(): + strengths.append("Uses threat modeling methodology") + else: + recommendations.append("Apply threat modeling framework (e.g., STRIDE)") + + elif 'test' in artifact_type: + if 'pass' in content.lower() and 'fail' in content.lower(): + strengths.append("Includes test criteria") + + score = max(0, 100 - (len(issues) * 20)) + + return { + 'score': score, + 'issues': issues, + 'strengths': strengths, + 'recommendations': recommendations, + 'has_description': description is not None + } + + +def check_industry_standards(content: str, artifact_type: str) -> Dict[str, Any]: + """Check alignment with industry standards and frameworks""" + strengths = [] + recommendations = [] + referenced_standards = [] + + # Common industry standards + standards = { + 'TOGAF': r'\bTOGAF\b', + 'ISO 27001': r'\bISO\s*27001\b', + 'NIST': r'\bNIST\b', + 'PCI-DSS': r'\bPCI[-\s]?DSS\b', + 'GDPR': r'\bGDPR\b', + 'SOC 2': r'\bSOC\s*2\b', + 'HIPAA': r'\bHIPAA\b', + 'SAFe': r'\bSAFe\b', + 'ITIL': r'\bITIL\b', + 'COBIT': r'\bCOBIT\b', + 'PMBOK': r'\bPMBOK\b', + 'OWASP': r'\bOWASP\b' + } + + for standard, pattern in standards.items(): + if re.search(pattern, content, re.IGNORECASE): + referenced_standards.append(standard) + + if referenced_standards: + strengths.append(f"References industry standards: {', '.join(referenced_standards)}") + else: + # Suggest relevant standards based on artifact type + if 'security' in artifact_type or 'threat' in artifact_type: + recommendations.append("Consider referencing security standards (ISO 27001, NIST, OWASP)") + elif 'architecture' in artifact_type: + recommendations.append("Consider referencing architecture frameworks (TOGAF, Zachman)") + elif 'governance' in artifact_type or 'portfolio' in artifact_type: + recommendations.append("Consider referencing governance frameworks (COBIT, PMBOK)") + + score = 100 if referenced_standards else 70 + + return { + 'score': score, + 'referenced_standards': referenced_standards, + 'strengths': strengths, + 'recommendations': recommendations + } + + +def calculate_readiness_score(review_results: Dict[str, Any]) -> int: + """Calculate overall readiness score""" + scores = [] + weights = [] + + # Content completeness (35%) + scores.append(review_results['completeness']['score']) + weights.append(0.35) + + # Professional quality (25%) + scores.append(review_results['professional_quality']['score']) + weights.append(0.25) + + # Best practices (25%) + scores.append(review_results['best_practices']['score']) + weights.append(0.25) + + # Industry standards (15%) + scores.append(review_results['industry_standards']['score']) + weights.append(0.15) + + readiness_score = sum(s * w for s, w in zip(scores, weights)) + return int(readiness_score) + + +def determine_quality_rating(readiness_score: int) -> str: + """Determine quality rating from readiness score""" + if readiness_score >= 90: + return "Excellent" + elif readiness_score >= 75: + return "Good" + elif readiness_score >= 60: + return "Fair" + elif readiness_score >= 40: + return "Needs Improvement" + else: + return "Poor" + + +def generate_summary_recommendations(review_results: Dict[str, Any]) -> List[str]: + """Generate prioritized summary recommendations""" + all_recommendations = [] + + # Critical issues first + for category in ['completeness', 'professional_quality', 'best_practices']: + for issue in review_results[category].get('issues', []): + all_recommendations.append(f"🔴 CRITICAL: {issue}") + + # Standard recommendations + for category in ['completeness', 'professional_quality', 'best_practices', 'industry_standards']: + for rec in review_results[category].get('recommendations', []): + if rec not in all_recommendations: # Avoid duplicates + all_recommendations.append(f"🟡 {rec}") + + return all_recommendations[:10] # Top 10 recommendations + + +def review_artifact( + artifact_path: str, + artifact_type: Optional[str] = None, + review_level: str = 'standard', + focus_areas: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Review artifact content for quality and best practices + + Args: + artifact_path: Path to artifact file + artifact_type: Type of artifact (auto-detected if not provided) + review_level: Review depth (quick, standard, comprehensive) + focus_areas: Specific areas to focus on + + Returns: + Review report with quality assessment and recommendations + """ + file_path = Path(artifact_path) + + if not file_path.exists(): + return { + 'success': False, + 'error': f"Artifact file not found: {artifact_path}", + 'quality_rating': 'N/A', + 'readiness_score': 0 + } + + with open(file_path, 'r') as f: + content = f.read() + + file_format = file_path.suffix.lstrip('.') + if file_format not in ['yaml', 'yml', 'md']: + return { + 'success': False, + 'error': f"Unsupported file format: {file_format}", + 'quality_rating': 'N/A', + 'readiness_score': 0 + } + + # Detect artifact type + detected_type = detect_artifact_type(file_path, content) + final_type = artifact_type or detected_type or "unknown" + + # Parse YAML if applicable + data = {} + if file_format in ['yaml', 'yml']: + try: + data = yaml.safe_load(content) + except: + data = {} + + # Initialize review results + review_results = { + 'artifact_path': str(file_path.absolute()), + 'artifact_type': final_type, + 'file_format': file_format, + 'review_level': review_level, + 'reviewed_at': datetime.now().isoformat() + } + + # Perform reviews + review_results['completeness'] = analyze_content_completeness(content, data, file_format) + review_results['professional_quality'] = analyze_professional_quality(content, file_format) + review_results['best_practices'] = check_best_practices(content, final_type, data) + review_results['industry_standards'] = check_industry_standards(content, final_type) + + # Calculate overall scores + readiness_score = calculate_readiness_score(review_results) + quality_rating = determine_quality_rating(readiness_score) + + # Generate summary + summary_recommendations = generate_summary_recommendations(review_results) + + # Collect all strengths + all_strengths = [] + for category in ['completeness', 'professional_quality', 'best_practices', 'industry_standards']: + all_strengths.extend(review_results[category].get('strengths', [])) + + return { + 'success': True, + 'review_results': review_results, + 'readiness_score': readiness_score, + 'quality_rating': quality_rating, + 'summary_recommendations': summary_recommendations, + 'strengths': all_strengths[:10] # Top 10 strengths + } + + +def main(): + """Main entry point for artifact.review skill""" + parser = argparse.ArgumentParser( + description='AI-powered artifact content review for quality and best practices' + ) + parser.add_argument( + 'artifact_path', + type=str, + help='Path to artifact file to review' + ) + parser.add_argument( + '--artifact-type', + type=str, + help='Type of artifact (auto-detected if not provided)' + ) + parser.add_argument( + '--review-level', + type=str, + choices=['quick', 'standard', 'comprehensive'], + default='standard', + help='Review depth level' + ) + parser.add_argument( + '--output', + type=str, + help='Save review report to file' + ) + + args = parser.parse_args() + + # Review artifact + result = review_artifact( + artifact_path=args.artifact_path, + artifact_type=args.artifact_type, + review_level=args.review_level + ) + + # Save to file if requested + if args.output: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'w') as f: + yaml.dump(result, f, default_flow_style=False, sort_keys=False) + print(f"\nReview report saved to: {output_path}") + + # Print report + if not result['success']: + print(f"\n{'='*70}") + print(f"✗ Review Failed") + print(f"{'='*70}") + print(f"Error: {result['error']}") + print(f"{'='*70}\n") + return 1 + + rr = result['review_results'] + + print(f"\n{'='*70}") + print(f"Artifact Content Review Report") + print(f"{'='*70}") + print(f"Artifact: {rr['artifact_path']}") + print(f"Type: {rr['artifact_type']}") + print(f"Review Level: {rr['review_level']}") + print(f"") + print(f"Quality Rating: {result['quality_rating']}") + print(f"Readiness Score: {result['readiness_score']}/100") + print(f"") + + # Content Completeness + comp = rr['completeness'] + print(f"Content Completeness: {comp['score']}/100") + print(f" Word Count: {comp['word_count']}") + print(f" Placeholders: {comp['placeholder_count']}") + if comp['strengths']: + for strength in comp['strengths']: + print(f" ✅ {strength}") + if comp['issues']: + for issue in comp['issues']: + print(f" ❌ {issue}") + print() + + # Professional Quality + prof = rr['professional_quality'] + print(f"Professional Quality: {prof['score']}/100") + if prof['strengths']: + for strength in prof['strengths']: + print(f" ✅ {strength}") + if prof['issues']: + for issue in prof['issues']: + print(f" ❌ {issue}") + print() + + # Best Practices + bp = rr['best_practices'] + print(f"Best Practices: {bp['score']}/100") + if bp['strengths']: + for strength in bp['strengths']: + print(f" ✅ {strength}") + if bp['issues']: + for issue in bp['issues']: + print(f" ❌ {issue}") + print() + + # Industry Standards + ist = rr['industry_standards'] + print(f"Industry Standards: {ist['score']}/100") + if ist['referenced_standards']: + print(f" ✅ References: {', '.join(ist['referenced_standards'])}") + if ist['strengths']: + for strength in ist['strengths']: + print(f" ✅ {strength}") + print() + + # Top Recommendations + print(f"Top Recommendations:") + for rec in result['summary_recommendations']: + print(f" {rec}") + print() + + # Overall Assessment + print(f"Overall Assessment:") + if result['readiness_score'] >= 90: + print(f" ✅ Excellent quality - ready for approval/publication") + elif result['readiness_score'] >= 75: + print(f" ✅ Good quality - minor improvements recommended") + elif result['readiness_score'] >= 60: + print(f" 🟡 Fair quality - needs refinement before approval") + elif result['readiness_score'] >= 40: + print(f" 🟠 Needs improvement - significant work required") + else: + print(f" 🔴 Poor quality - major revision needed") + + print(f"{'='*70}\n") + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/skills/artifact.review/skill.yaml b/skills/artifact.review/skill.yaml new file mode 100644 index 0000000..8c954df --- /dev/null +++ b/skills/artifact.review/skill.yaml @@ -0,0 +1,100 @@ +name: artifact.review +version: 0.1.0 +description: > + AI-powered artifact content review for quality, completeness, and best practices. + Analyzes artifact content against industry standards, provides quality scoring, + and generates actionable recommendations for improvement. + +inputs: + - name: artifact_path + type: string + required: true + description: Path to the artifact file to review + + - name: artifact_type + type: string + required: false + description: Type of artifact (auto-detected from filename/content if not provided) + + - name: review_level + type: string + required: false + default: standard + description: Review depth (quick, standard, comprehensive) + + - name: focus_areas + type: array + required: false + description: Specific areas to focus review on (e.g., security, compliance, completeness) + +outputs: + - name: review_report + type: object + description: Detailed review with quality assessment and recommendations + + - name: quality_rating + type: string + description: Overall quality rating (Excellent, Good, Fair, Needs Improvement, Poor) + + - name: readiness_score + type: number + description: Readiness score from 0-100 for approval/publication + +dependencies: + - artifact.define + - artifact.validate + +entrypoints: + - command: /skill/artifact/review + handler: artifact_review.py + runtime: python + description: > + AI-powered review of artifact content quality. Analyzes completeness, + professional quality, best practices compliance, and industry standards + alignment. Provides detailed feedback and actionable recommendations. + parameters: + - name: artifact_path + type: string + required: true + description: Path to artifact file + - name: artifact_type + type: string + required: false + description: Artifact type (auto-detected if not provided) + - name: review_level + type: string + required: false + default: standard + description: Review depth (quick, standard, comprehensive) + - name: focus_areas + type: array + required: false + description: Specific review focus areas + permissions: + - filesystem:read + +status: active + +tags: + - artifacts + - review + - quality + - ai-powered + - tier2 + - phase2 + +# This skill's own artifact metadata +artifact_metadata: + produces: + - type: review-report + description: Detailed artifact content review with quality assessment + file_pattern: "*-review-report.yaml" + content_type: application/yaml + + consumes: + - type: "*" + description: Reviews any artifact type from the registry + file_pattern: "**/*.{yaml,yml,md}" + - type: artifact-type-description + description: References comprehensive artifact descriptions for quality criteria + file_pattern: "artifact_descriptions/*.md" diff --git a/skills/artifact.scaffold/README.md b/skills/artifact.scaffold/README.md new file mode 100644 index 0000000..34eb01b --- /dev/null +++ b/skills/artifact.scaffold/README.md @@ -0,0 +1,166 @@ +# artifact.scaffold + +Generate new artifact templates automatically from metadata inputs. + +## Overview + +The `artifact.scaffold` skill creates fully compliant artifact descriptors in one call. It generates valid `.artifact.yaml` files, assigns auto-incremented versions starting at 0.1.0, and registers artifacts in the artifacts registry. + +## Features + +- **Automatic Generation**: Creates artifact YAML files from metadata inputs +- **Schema Definition**: Supports field definitions with types, descriptions, and required flags +- **Inheritance**: Supports extending from base artifacts +- **Registry Management**: Automatically registers artifacts in `registry/artifacts.json` +- **Validation**: Optional `--validate` flag to validate generated artifacts +- **Version Management**: Auto-assigns version 0.1.0 to new artifacts + +## Usage + +### Basic Example + +```bash +python3 skills/artifact.scaffold/artifact_scaffold.py \ + --id "new.artifact" \ + --category "report" +``` + +### With Field Definitions + +```bash +python3 skills/artifact.scaffold/artifact_scaffold.py \ + --id "new.artifact" \ + --category "report" \ + --fields '[{"name":"summary","type":"string","description":"Summary field","required":true}]' +``` + +### With Inheritance and Validation + +```bash +python3 skills/artifact.scaffold/artifact_scaffold.py \ + --id "new.artifact" \ + --category "report" \ + --extends "base.artifact" \ + --fields '[{"name":"summary","type":"string"}]' \ + --validate +``` + +### Custom Output Path + +```bash +python3 skills/artifact.scaffold/artifact_scaffold.py \ + --id "new.artifact" \ + --category "report" \ + --output "custom/path/artifact.yaml" +``` + +## Input Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `--id` | string | Yes | Unique identifier for the artifact (e.g., "new.artifact") | +| `--category` | string | Yes | Category/type of artifact (e.g., "report", "specification") | +| `--extends` | string | No | Base artifact to extend from | +| `--fields` | JSON array | No | Field definitions with name, type, description, and required properties | +| `--output` | string | No | Custom output path for the artifact file | +| `--validate` | flag | No | Validate the artifact after generation | + +## Field Definition Format + +Fields are provided as a JSON array with the following structure: + +```json +[ + { + "name": "field_name", + "type": "string|number|boolean|object|array", + "description": "Field description", + "required": true|false + } +] +``` + +## Output + +The skill outputs a JSON response with the following structure: + +```json +{ + "ok": true, + "status": "success", + "artifact_id": "new.artifact", + "file_path": "/path/to/artifact.yaml", + "version": "0.1.0", + "category": "report", + "registry_path": "/path/to/registry/artifacts.json", + "artifacts_registered": 1, + "validation": { + "valid": true, + "errors": [], + "warnings": [] + } +} +``` + +## Generated Artifact Structure + +The skill generates artifact YAML files with the following structure: + +```yaml +id: new.artifact +version: 0.1.0 +category: report +created_at: '2025-10-26T00:00:00.000000Z' +metadata: + description: new.artifact artifact + tags: + - report +extends: base.artifact # Optional +schema: + type: object + properties: + summary: + type: string + description: Summary field + required: + - summary +``` + +## Registry Management + +Artifacts are automatically registered in `registry/artifacts.json`: + +```json +{ + "registry_version": "1.0.0", + "generated_at": "2025-10-26T00:00:00.000000Z", + "artifacts": [ + { + "id": "new.artifact", + "version": "0.1.0", + "category": "report", + "created_at": "2025-10-26T00:00:00.000000Z", + "description": "new.artifact artifact", + "tags": ["report"], + "extends": "base.artifact", + "schema": { ... } + } + ] +} +``` + +## Dependencies + +- `artifact.define`: For artifact type definitions and validation + +## Status + +**Active** - Ready for production use + +## Tags + +- artifacts +- scaffolding +- generation +- templates +- metadata diff --git a/skills/artifact.scaffold/__init__.py b/skills/artifact.scaffold/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/artifact.scaffold/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/artifact.scaffold/artifact_scaffold.py b/skills/artifact.scaffold/artifact_scaffold.py new file mode 100755 index 0000000..3cde480 --- /dev/null +++ b/skills/artifact.scaffold/artifact_scaffold.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +""" +artifact_scaffold.py - Generate new artifact templates automatically from metadata inputs + +Creates compliant artifact descriptors, registers them in the registry, and optionally validates them. +""" + +import os +import sys +import json +import yaml +import argparse +from typing import Dict, Any, List, Optional +from pathlib import Path +from datetime import datetime + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.errors import format_error_response + +logger = setup_logger(__name__) + +# Default artifact directories +ARTIFACTS_DIR = os.path.join(BASE_DIR, "artifacts") +REGISTRY_DIR = os.path.join(BASE_DIR, "registry") +ARTIFACTS_REGISTRY_FILE = os.path.join(REGISTRY_DIR, "artifacts.json") + + +def ensure_directories(): + """Ensure required directories exist""" + os.makedirs(ARTIFACTS_DIR, exist_ok=True) + os.makedirs(REGISTRY_DIR, exist_ok=True) + + +def load_artifacts_registry() -> Dict[str, Any]: + """Load the artifacts registry, or create a new one if it doesn't exist""" + if os.path.exists(ARTIFACTS_REGISTRY_FILE): + try: + with open(ARTIFACTS_REGISTRY_FILE, 'r') as f: + return json.load(f) + except Exception as e: + logger.warning(f"Failed to load artifacts registry: {e}") + return create_empty_registry() + else: + return create_empty_registry() + + +def create_empty_registry() -> Dict[str, Any]: + """Create a new empty artifacts registry""" + return { + "registry_version": "1.0.0", + "generated_at": datetime.utcnow().isoformat() + "Z", + "artifacts": [] + } + + +def save_artifacts_registry(registry: Dict[str, Any]): + """Save the artifacts registry""" + registry["generated_at"] = datetime.utcnow().isoformat() + "Z" + with open(ARTIFACTS_REGISTRY_FILE, 'w') as f: + json.dump(registry, f, indent=2) + logger.info(f"Saved artifacts registry to {ARTIFACTS_REGISTRY_FILE}") + + +def generate_artifact_yaml( + artifact_id: str, + category: str, + extends: Optional[str] = None, + fields: Optional[List[Dict[str, str]]] = None, + version: str = "0.1.0" +) -> Dict[str, Any]: + """ + Generate an artifact YAML structure + + Args: + artifact_id: Unique identifier for the artifact (e.g., "new.artifact") + category: Category/type of artifact (e.g., "report", "specification") + extends: Optional base artifact to extend from + fields: List of field definitions with name and type + version: Semantic version (default: 0.1.0) + + Returns: + Dictionary representing the artifact structure + """ + artifact = { + "id": artifact_id, + "version": version, + "category": category, + "created_at": datetime.utcnow().isoformat() + "Z", + "metadata": { + "description": f"{artifact_id} artifact", + "tags": [category] + } + } + + if extends: + artifact["extends"] = extends + + if fields: + artifact["schema"] = { + "type": "object", + "properties": {}, + "required": [] + } + + for field in fields: + field_name = field.get("name", "") + field_type = field.get("type", "string") + field_description = field.get("description", f"{field_name} field") + field_required = field.get("required", False) + + artifact["schema"]["properties"][field_name] = { + "type": field_type, + "description": field_description + } + + if field_required: + artifact["schema"]["required"].append(field_name) + + return artifact + + +def get_artifact_filename(artifact_id: str) -> str: + """ + Generate filename for artifact YAML file + + Args: + artifact_id: The artifact ID (e.g., "new.artifact") + + Returns: + Filename in format: {artifact_id}.artifact.yaml + """ + # Replace dots with hyphens for filename + safe_id = artifact_id.replace(".", "-") + return f"{safe_id}.artifact.yaml" + + +def save_artifact_yaml(artifact: Dict[str, Any], output_path: Optional[str] = None) -> str: + """ + Save artifact to YAML file + + Args: + artifact: The artifact dictionary + output_path: Optional custom output path + + Returns: + Path to the saved file + """ + artifact_id = artifact["id"] + + if output_path: + file_path = output_path + else: + filename = get_artifact_filename(artifact_id) + file_path = os.path.join(ARTIFACTS_DIR, filename) + + with open(file_path, 'w') as f: + yaml.dump(artifact, f, default_flow_style=False, sort_keys=False) + + logger.info(f"Saved artifact to {file_path}") + return file_path + + +def register_artifact(artifact: Dict[str, Any]) -> Dict[str, Any]: + """ + Register artifact in the artifacts registry + + Args: + artifact: The artifact dictionary + + Returns: + The updated registry + """ + registry = load_artifacts_registry() + + # Check if artifact already exists + artifact_id = artifact["id"] + existing_idx = None + for idx, reg_artifact in enumerate(registry["artifacts"]): + if reg_artifact["id"] == artifact_id: + existing_idx = idx + break + + # Create registry entry + registry_entry = { + "id": artifact["id"], + "version": artifact["version"], + "category": artifact["category"], + "created_at": artifact["created_at"], + "description": artifact.get("metadata", {}).get("description", ""), + "tags": artifact.get("metadata", {}).get("tags", []) + } + + if "extends" in artifact: + registry_entry["extends"] = artifact["extends"] + + if "schema" in artifact: + registry_entry["schema"] = artifact["schema"] + + # Update or add entry + if existing_idx is not None: + registry["artifacts"][existing_idx] = registry_entry + logger.info(f"Updated artifact {artifact_id} in registry") + else: + registry["artifacts"].append(registry_entry) + logger.info(f"Added artifact {artifact_id} to registry") + + save_artifacts_registry(registry) + return registry + + +def scaffold_artifact( + artifact_id: str, + category: str, + extends: Optional[str] = None, + fields: Optional[List[Dict[str, str]]] = None, + output_path: Optional[str] = None, + validate: bool = False +) -> Dict[str, Any]: + """ + Main scaffolding function + + Args: + artifact_id: Unique identifier for the artifact + category: Category/type of artifact + extends: Optional base artifact to extend from + fields: List of field definitions + output_path: Optional custom output path + validate: Whether to run validation after scaffolding + + Returns: + Result dictionary with status and details + """ + try: + ensure_directories() + + # Generate artifact structure + artifact = generate_artifact_yaml( + artifact_id=artifact_id, + category=category, + extends=extends, + fields=fields + ) + + # Save to file + file_path = save_artifact_yaml(artifact, output_path) + + # Register in artifacts registry + registry = register_artifact(artifact) + + result = { + "ok": True, + "status": "success", + "artifact_id": artifact_id, + "file_path": file_path, + "version": artifact["version"], + "category": category, + "registry_path": ARTIFACTS_REGISTRY_FILE, + "artifacts_registered": len(registry["artifacts"]) + } + + # Optional validation + if validate: + validation_result = validate_artifact(file_path) + result["validation"] = validation_result + + return result + + except Exception as e: + logger.error(f"Failed to scaffold artifact: {e}", exc_info=True) + return { + "ok": False, + "status": "failed", + "error": str(e), + "details": format_error_response(e) + } + + +def validate_artifact(file_path: str) -> Dict[str, Any]: + """ + Validate an artifact YAML file + + Args: + file_path: Path to the artifact YAML file + + Returns: + Validation result dictionary + """ + try: + with open(file_path, 'r') as f: + artifact = yaml.safe_load(f) + + errors = [] + warnings = [] + + # Required fields + required_fields = ["id", "version", "category", "created_at"] + for field in required_fields: + if field not in artifact: + errors.append(f"Missing required field: {field}") + + # Version format check + if "version" in artifact: + version = artifact["version"] + parts = version.split(".") + if len(parts) != 3 or not all(p.isdigit() for p in parts): + warnings.append(f"Version {version} may not follow semantic versioning (X.Y.Z)") + + # Category check + if "category" in artifact and not artifact["category"]: + warnings.append("Category is empty") + + # Schema validation + if "schema" in artifact: + schema = artifact["schema"] + if "properties" not in schema: + warnings.append("Schema missing 'properties' field") + + is_valid = len(errors) == 0 + + return { + "valid": is_valid, + "errors": errors, + "warnings": warnings, + "file_path": file_path + } + + except Exception as e: + return { + "valid": False, + "errors": [f"Failed to validate: {str(e)}"], + "warnings": [], + "file_path": file_path + } + + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Generate new artifact templates from metadata" + ) + + parser.add_argument( + "--id", + required=True, + help="Artifact ID (e.g., 'new.artifact')" + ) + + parser.add_argument( + "--category", + required=True, + help="Artifact category (e.g., 'report', 'specification')" + ) + + parser.add_argument( + "--extends", + help="Base artifact to extend from (optional)" + ) + + parser.add_argument( + "--fields", + help="JSON string of field definitions (e.g., '[{\"name\":\"summary\",\"type\":\"string\"}]')" + ) + + parser.add_argument( + "--output", + help="Custom output path for the artifact file" + ) + + parser.add_argument( + "--validate", + action="store_true", + help="Validate the artifact after generation" + ) + + args = parser.parse_args() + + # Parse fields if provided + fields = None + if args.fields: + try: + fields = json.loads(args.fields) + except json.JSONDecodeError as e: + print(json.dumps({ + "ok": False, + "status": "failed", + "error": f"Invalid JSON for fields: {e}" + }, indent=2)) + sys.exit(1) + + # Scaffold the artifact + result = scaffold_artifact( + artifact_id=args.id, + category=args.category, + extends=args.extends, + fields=fields, + output_path=args.output, + validate=args.validate + ) + + # Output result as JSON + print(json.dumps(result, indent=2)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok", False) else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/artifact.scaffold/skill.yaml b/skills/artifact.scaffold/skill.yaml new file mode 100644 index 0000000..387deab --- /dev/null +++ b/skills/artifact.scaffold/skill.yaml @@ -0,0 +1,136 @@ +name: artifact.scaffold +version: 0.1.0 +description: > + Generate new artifact templates automatically from metadata inputs. + Creates fully compliant artifact descriptors with auto-incremented versions, + saves them as .artifact.yaml files, and registers them in the artifacts registry. + Supports optional validation of generated artifacts. + +inputs: + - name: id + type: string + required: true + description: Unique identifier for the artifact (e.g., "new.artifact") + + - name: category + type: string + required: true + description: Category/type of artifact (e.g., "report", "specification") + + - name: extends + type: string + required: false + description: Optional base artifact to extend from (e.g., "base.artifact") + + - name: fields + type: array + required: false + description: List of field definitions with name, type, description, and required properties + + - name: output + type: string + required: false + description: Custom output path for the artifact file (defaults to artifacts/{id}.artifact.yaml) + + - name: validate + type: boolean + required: false + default: false + description: Whether to validate the artifact after generation + +outputs: + - name: artifact_id + type: string + description: ID of the generated artifact + + - name: file_path + type: string + description: Path to the generated artifact YAML file + + - name: version + type: string + description: Version assigned to the artifact (default 0.1.0) + + - name: category + type: string + description: Category of the artifact + + - name: registry_path + type: string + description: Path to the artifacts registry + + - name: artifacts_registered + type: integer + description: Total number of artifacts in the registry + + - name: validation + type: object + required: false + description: Validation results if --validate flag was used + +dependencies: + - artifact.define + +entrypoints: + - command: /skill/artifact/scaffold + handler: artifact_scaffold.py + runtime: python + description: > + Generate a new artifact template from metadata. Creates a valid .artifact.yaml + file with the specified structure, registers it in the artifacts registry, + and optionally validates the output. + parameters: + - name: id + type: string + required: true + description: Artifact ID in namespace.name format + - name: category + type: string + required: true + description: Artifact category + - name: extends + type: string + required: false + description: Base artifact to extend + - name: fields + type: array + required: false + description: Field definitions as JSON array + - name: output + type: string + required: false + description: Custom output path + - name: validate + type: boolean + required: false + description: Validate after generation + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: + - artifacts + - scaffolding + - generation + - templates + - metadata + +artifact_metadata: + produces: + - type: artifact-definition + description: Generated artifact YAML descriptor file + file_pattern: "*.artifact.yaml" + content_type: application/yaml + + - type: artifact-registry + description: Updated artifacts registry with new entries + file_pattern: "registry/artifacts.json" + content_type: application/json + + consumes: + - type: artifact-metadata + description: Optional artifact metadata for extension + file_pattern: "*.artifact.yaml" + content_type: application/yaml diff --git a/skills/artifact.validate.types/SKILL.md b/skills/artifact.validate.types/SKILL.md new file mode 100644 index 0000000..12b3520 --- /dev/null +++ b/skills/artifact.validate.types/SKILL.md @@ -0,0 +1,378 @@ +# artifact.validate.types + +## Overview + +Validates artifact type names against the Betty Framework registry and returns complete metadata for each type. Provides intelligent fuzzy matching and suggestions for invalid types. + +**Version**: 0.1.0 +**Status**: active + +## Purpose + +This skill is critical for ensuring skills reference valid artifact types before creation. It validates artifact type names against `registry/artifact_types.json`, retrieves complete metadata (file_pattern, content_type, schema), and suggests alternatives for invalid types using three fuzzy matching strategies: + +1. **Singular/Plural Detection** (high confidence) - Detects "data-flow-diagram" vs "data-flow-diagrams" +2. **Generic vs Specific Variants** (medium confidence) - Suggests "logical-data-model" for "data-model" +3. **Levenshtein Distance** (low confidence) - Catches typos like "thret-model" → "threat-model" + +This skill is specifically designed to be called by `meta.skill` during Step 2 (Validate Artifact Types) of the skill creation workflow. + +## Inputs + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `artifact_types` | array | Yes | - | List of artifact type names to validate | +| `check_schemas` | boolean | No | `true` | Whether to verify schema files exist on filesystem | +| `suggest_alternatives` | boolean | No | `true` | Whether to suggest similar types for invalid ones | +| `max_suggestions` | number | No | `3` | Maximum number of suggestions per invalid type | + +## Outputs + +| Output | Type | Description | +|--------|------|-------------| +| `validation_results` | object | Validation results for each artifact type with complete metadata | +| `all_valid` | boolean | Whether all artifact types are valid | +| `invalid_types` | array | List of artifact types that don't exist in registry | +| `suggestions` | object | Suggested alternatives for each invalid type | +| `warnings` | array | List of warnings (e.g., schema file missing) | + +## Artifact Metadata + +### Produces +- **validation-report** (`*.validation.json`) - Validation results with metadata and suggestions + +### Consumes +None - reads directly from registry files + +## Usage + +### Example 1: Validate Single Artifact Type + +```bash +python artifact_validate_types.py \ + --artifact_types '["threat-model"]' \ + --check_schemas true +``` + +**Output:** +```json +{ + "validation_results": { + "threat-model": { + "valid": true, + "file_pattern": "*.threat-model.yaml", + "content_type": "application/yaml", + "schema": "schemas/artifacts/threat-model-schema.json", + "description": "Threat model (STRIDE, attack trees)..." + } + }, + "all_valid": true, + "invalid_types": [], + "suggestions": {}, + "warnings": [] +} +``` + +### Example 2: Invalid Type with Suggestions + +```bash +python artifact_validate_types.py \ + --artifact_types '["data-flow-diagram", "threat-model"]' \ + --suggest_alternatives true +``` + +**Output:** +```json +{ + "validation_results": { + "data-flow-diagram": { + "valid": false + }, + "threat-model": { + "valid": true, + "file_pattern": "*.threat-model.yaml", + "content_type": "application/yaml", + "schema": "schemas/artifacts/threat-model-schema.json" + } + }, + "all_valid": false, + "invalid_types": ["data-flow-diagram"], + "suggestions": { + "data-flow-diagram": [ + { + "type": "data-flow-diagrams", + "reason": "Plural form", + "confidence": "high" + }, + { + "type": "dataflow-diagram", + "reason": "Similar spelling", + "confidence": "low" + } + ] + }, + "warnings": [], + "ok": false, + "status": "validation_failed" +} +``` + +### Example 3: Multiple Invalid Types with Generic → Specific Suggestions + +```bash +python artifact_validate_types.py \ + --artifact_types '["data-model", "api-spec", "test-result"]' \ + --max_suggestions 3 +``` + +**Output:** +```json +{ + "all_valid": false, + "invalid_types": ["data-model", "api-spec"], + "suggestions": { + "data-model": [ + { + "type": "logical-data-model", + "reason": "Specific variant of model", + "confidence": "medium" + }, + { + "type": "physical-data-model", + "reason": "Specific variant of model", + "confidence": "medium" + }, + { + "type": "enterprise-data-model", + "reason": "Specific variant of model", + "confidence": "medium" + } + ], + "api-spec": [ + { + "type": "openapi-spec", + "reason": "Specific variant of spec", + "confidence": "medium" + }, + { + "type": "asyncapi-spec", + "reason": "Specific variant of spec", + "confidence": "medium" + } + ] + }, + "validation_results": { + "test-result": { + "valid": true, + "file_pattern": "*.test-result.json", + "content_type": "application/json" + } + } +} +``` + +### Example 4: Save Validation Report to File + +```bash +python artifact_validate_types.py \ + --artifact_types '["threat-model", "architecture-overview"]' \ + --output validation-results.validation.json +``` + +Creates `validation-results.validation.json` with complete validation report. + +## Integration with meta.skill + +The `meta.skill` agent calls this skill in Step 2 of its workflow: + +```yaml +# meta.skill workflow Step 2 +2. **Validate Artifact Types** + - Extract artifact types from skill description + - Call artifact.validate.types with all types + - If all_valid == false: + → Display suggestions to user + → Ask user to confirm correct types + → HALT until types are validated + - Store validated metadata for use in skill.yaml generation +``` + +**Example Integration:** + +```python +# meta.skill calls artifact.validate.types +result = subprocess.run([ + 'python', 'skills/artifact.validate.types/artifact_validate_types.py', + '--artifact_types', json.dumps(["threat-model", "data-flow-diagrams"]), + '--suggest_alternatives', 'true' +], capture_output=True, text=True) + +validation = json.loads(result.stdout) + +if not validation['all_valid']: + print(f"❌ Invalid artifact types: {validation['invalid_types']}") + for invalid_type, suggestions in validation['suggestions'].items(): + print(f"\n Suggestions for '{invalid_type}':") + for s in suggestions: + print(f" - {s['type']} ({s['confidence']} confidence): {s['reason']}") + # HALT skill creation +else: + print("✅ All artifact types validated") + # Continue with skill.yaml generation using validated metadata +``` + +## Fuzzy Matching Strategies + +### Strategy 1: Singular/Plural Detection (High Confidence) + +Detects when a user forgets the "s": + +| Invalid Type | Suggested Type | Reason | +|-------------|----------------|--------| +| `data-flow-diagram` | `data-flow-diagrams` | Plural form | +| `threat-models` | `threat-model` | Singular form | + +### Strategy 2: Generic vs Specific Variants (Medium Confidence) + +Suggests specific variants when a generic term is used: + +| Invalid Type | Suggested Types | +|-------------|-----------------| +| `data-model` | `logical-data-model`, `physical-data-model`, `enterprise-data-model` | +| `api-spec` | `openapi-spec`, `asyncapi-spec`, `graphql-spec` | +| `architecture-diagram` | `system-architecture-diagram`, `component-architecture-diagram` | + +### Strategy 3: Levenshtein Distance (Low Confidence) + +Catches typos and misspellings (60%+ similarity): + +| Invalid Type | Suggested Type | Similarity | +|-------------|----------------|------------| +| `thret-model` | `threat-model` | ~90% | +| `architecure-overview` | `architecture-overview` | ~85% | +| `api-specfication` | `api-specification` | ~92% | + +## Error Handling + +### Missing Registry File + +```json +{ + "ok": false, + "status": "error", + "error": "Artifact registry not found: registry/artifact_types.json" +} +``` + +**Resolution**: Ensure you're running from the Betty Framework root directory. + +### Invalid JSON in artifact_types Parameter + +```json +{ + "ok": false, + "status": "error", + "error": "Invalid JSON: Expecting ',' delimiter: line 1 column 15 (char 14)" +} +``` + +**Resolution**: Ensure artifact_types is a valid JSON array with proper quoting. + +### Corrupted Registry File + +```json +{ + "ok": false, + "status": "error", + "error": "Invalid JSON in registry file: ..." +} +``` + +**Resolution**: Validate and fix `registry/artifact_types.json` syntax. + +## Performance + +- **Single type validation**: <100ms +- **20 types validation**: <1 second +- **All 409 types validation**: <5 seconds + +Memory usage is minimal as registry is loaded once and indexed by name for O(1) lookups. + +## Dependencies + +- **Python 3.7+** +- **PyYAML** - For reading registry +- **difflib** - For fuzzy matching (Python stdlib) +- **jsonschema** - For validation (optional) + +## Testing + +Run the test suite: + +```bash +cd skills/artifact.validate.types +python test_artifact_validate_types.py +``` + +**Test Coverage:** +- ✅ Valid artifact type validation +- ✅ Invalid artifact type detection +- ✅ Singular/plural suggestion +- ✅ Generic → specific suggestion +- ✅ Typo detection with Levenshtein distance +- ✅ Max suggestions limit +- ✅ Schema file existence checking +- ✅ Empty input handling +- ✅ Mixed valid/invalid types + +## Quality Standards + +- **Accuracy**: 100% for exact matches in registry +- **Suggestion Quality**: >80% relevant for common mistakes +- **Performance**: <1s for 20 types, <100ms for single type +- **Schema Verification**: 100% accurate file existence check +- **Error Handling**: Graceful handling of corrupted registry files + +## Success Criteria + +- ✅ Validates all 409 artifact types correctly +- ✅ Provides accurate suggestions for common mistakes (singular/plural) +- ✅ Returns exact metadata from registry (file_pattern, content_type, schema) +- ✅ Detects missing schema files and warns appropriately +- ✅ Completes validation in <1 second for up to 20 types +- ✅ Fuzzy matching handles typos within 40% character difference + +## Troubleshooting + +### Skill returns all_valid=false but I think types are correct + +1. Check the exact spelling in `registry/artifact_types.json` +2. Look at suggestions - they often reveal plural/singular issues +3. Use `jq` to search registry: + ```bash + jq '.artifact_types[] | select(.name | contains("your-search"))' registry/artifact_types.json + ``` + +### Fuzzy matching isn't suggesting the type I expect + +1. Check if the type name follows patterns (ending in common suffix like "-model", "-spec") +2. Increase `max_suggestions` to see more options +3. The type might be too dissimilar (< 60% match threshold) + +### Schema warnings appearing for valid types + +This is normal if schema files haven't been created yet. Schema files are optional for many artifact types. Set `check_schemas=false` to suppress these warnings. + +## Related Skills + +- **artifact.define** - Define new artifact types +- **artifact.create** - Create artifact files +- **skill.define** - Validate skill manifests +- **registry.update** - Update skill registry + +## References + +- [Python difflib](https://docs.python.org/3/library/difflib.html) - Fuzzy string matching +- [Betty Artifact Registry](../../registry/artifact_types.json) - Source of truth for artifact types +- [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance) - String similarity algorithm +- [meta.skill Agent](../../agents/meta.skill/agent.yaml) - Primary consumer of this skill diff --git a/skills/artifact.validate.types/artifact_validate_types.py b/skills/artifact.validate.types/artifact_validate_types.py new file mode 100644 index 0000000..94cffba --- /dev/null +++ b/skills/artifact.validate.types/artifact_validate_types.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +""" +Artifact Type Validation Skill for Betty Framework + +Validates artifact type names against registry/artifact_types.json and provides +fuzzy matching suggestions for invalid types using: +- Singular/plural detection +- Generic vs specific variant matching +- Levenshtein distance for typos + +Usage: + python artifact_validate_types.py --artifact_types '["threat-model", "data-flow-diagram"]' +""" + +import argparse +import json +import logging +import os +import sys +from difflib import get_close_matches +from pathlib import Path +from typing import Any, Dict, List + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +def load_artifact_registry(registry_path: str = "registry/artifact_types.json") -> Dict[str, Any]: + """ + Load the artifact types registry. + + Args: + registry_path: Path to artifact_types.json + + Returns: + Dictionary with artifact types indexed by name + + Raises: + FileNotFoundError: If registry file doesn't exist + json.JSONDecodeError: If registry file is invalid JSON + """ + if not os.path.exists(registry_path): + raise FileNotFoundError(f"Artifact registry not found: {registry_path}") + + try: + with open(registry_path, 'r') as f: + registry = json.load(f) + + # Index by name for O(1) lookup + artifact_types_dict = { + artifact['name']: artifact + for artifact in registry.get('artifact_types', []) + } + + logger.info(f"Loaded {len(artifact_types_dict)} artifact types from registry") + return artifact_types_dict + + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in registry file: {e}") + raise + + +def find_similar_types( + invalid_type: str, + all_types: List[str], + max_suggestions: int = 3 +) -> List[Dict[str, str]]: + """ + Find similar artifact types using multiple strategies. + + Strategies: + 1. Singular/Plural variants (high confidence) + 2. Generic vs Specific variants (medium confidence) + 3. Levenshtein distance for typos (low confidence) + + Args: + invalid_type: The artifact type that wasn't found + all_types: List of all valid artifact type names + max_suggestions: Maximum number of suggestions to return + + Returns: + List of suggestion dictionaries with type, reason, and confidence + """ + suggestions = [] + + # Strategy 1: Singular/Plural variants + if invalid_type.endswith('s'): + singular = invalid_type[:-1] + if singular in all_types: + suggestions.append({ + 'type': singular, + 'reason': 'Singular form', + 'confidence': 'high' + }) + else: + plural = invalid_type + 's' + if plural in all_types: + suggestions.append({ + 'type': plural, + 'reason': 'Plural form', + 'confidence': 'high' + }) + + # Strategy 2: Generic vs Specific variants + # e.g., "data-model" → ["logical-data-model", "physical-data-model"] + if '-' in invalid_type: + parts = invalid_type.split('-') + base_term = parts[-1] # e.g., "model" from "data-model" + + # Find types that end with "-{base_term}" + matches = [t for t in all_types if t.endswith('-' + base_term)] + + for match in matches[:max_suggestions]: + if match not in [s['type'] for s in suggestions]: + suggestions.append({ + 'type': match, + 'reason': f'Specific variant of {base_term}', + 'confidence': 'medium' + }) + + # Strategy 3: Levenshtein distance for typos + close_matches = get_close_matches( + invalid_type, + all_types, + n=max_suggestions, + cutoff=0.6 # 60% similarity threshold + ) + + for match in close_matches: + if match not in [s['type'] for s in suggestions]: + suggestions.append({ + 'type': match, + 'reason': 'Similar spelling', + 'confidence': 'low' + }) + + return suggestions[:max_suggestions] + + +def validate_artifact_types( + artifact_types: List[str], + check_schemas: bool = True, + suggest_alternatives: bool = True, + max_suggestions: int = 3, + registry_path: str = "registry/artifact_types.json" +) -> Dict[str, Any]: + """ + Validate artifact types against the registry. + + Args: + artifact_types: List of artifact type names to validate + check_schemas: Whether to verify schema files exist + suggest_alternatives: Whether to suggest similar types for invalid ones + max_suggestions: Maximum suggestions per invalid type + registry_path: Path to artifact registry JSON file + + Returns: + Dictionary with validation results: + { + 'validation_results': {type_name: {valid, file_pattern, ...}}, + 'all_valid': bool, + 'invalid_types': [type_names], + 'suggestions': {type_name: [suggestions]}, + 'warnings': [warning_messages] + } + """ + # Load registry + artifact_types_dict = load_artifact_registry(registry_path) + all_type_names = list(artifact_types_dict.keys()) + + # Initialize results + results = {} + invalid_types = [] + suggestions_dict = {} + warnings = [] + + # Validate each type + for artifact_type in artifact_types: + if artifact_type in artifact_types_dict: + # Valid - get metadata + metadata = artifact_types_dict[artifact_type] + + # Check schema file exists (if check_schemas=true) + if check_schemas and metadata.get('schema'): + schema_path = metadata['schema'] + if not os.path.exists(schema_path): + warning_msg = f"Schema file missing: {schema_path}" + warnings.append(warning_msg) + logger.warning(warning_msg) + + results[artifact_type] = { + 'valid': True, + 'file_pattern': metadata.get('file_pattern'), + 'content_type': metadata.get('content_type'), + 'schema': metadata.get('schema'), + 'description': metadata.get('description') + } + + logger.info(f"✓ {artifact_type} - valid") + + else: + # Invalid - mark as invalid and find suggestions + results[artifact_type] = {'valid': False} + invalid_types.append(artifact_type) + + logger.warning(f"✗ {artifact_type} - not found in registry") + + # Generate suggestions if enabled + if suggest_alternatives: + suggestions = find_similar_types( + artifact_type, + all_type_names, + max_suggestions + ) + if suggestions: + suggestions_dict[artifact_type] = suggestions + logger.info( + f" Suggestions for '{artifact_type}': " + f"{', '.join(s['type'] for s in suggestions)}" + ) + + # Compile final results + return { + 'validation_results': results, + 'all_valid': len(invalid_types) == 0, + 'invalid_types': invalid_types, + 'suggestions': suggestions_dict, + 'warnings': warnings + } + + +def main(): + """Main entry point for artifact.validate.types skill.""" + parser = argparse.ArgumentParser( + description="Validate artifact types against Betty Framework registry" + ) + + parser.add_argument( + '--artifact_types', + type=str, + required=True, + help='JSON array of artifact type names to validate (e.g., \'["threat-model", "data-flow-diagram"]\')' + ) + + parser.add_argument( + '--check_schemas', + type=bool, + default=True, + help='Whether to verify schema files exist on filesystem (default: true)' + ) + + parser.add_argument( + '--suggest_alternatives', + type=bool, + default=True, + help='Whether to suggest similar types for invalid ones (default: true)' + ) + + parser.add_argument( + '--max_suggestions', + type=int, + default=3, + help='Maximum number of suggestions per invalid type (default: 3)' + ) + + parser.add_argument( + '--registry_path', + type=str, + default='registry/artifact_types.json', + help='Path to artifact registry file (default: registry/artifact_types.json)' + ) + + parser.add_argument( + '--output', + type=str, + help='Output file path for validation report (optional)' + ) + + args = parser.parse_args() + + try: + # Parse artifact_types JSON + artifact_types = json.loads(args.artifact_types) + + if not isinstance(artifact_types, list): + logger.error("artifact_types must be a JSON array") + sys.exit(1) + + logger.info(f"Validating {len(artifact_types)} artifact types...") + + # Perform validation + result = validate_artifact_types( + artifact_types=artifact_types, + check_schemas=args.check_schemas, + suggest_alternatives=args.suggest_alternatives, + max_suggestions=args.max_suggestions, + registry_path=args.registry_path + ) + + # Add metadata + result['ok'] = result['all_valid'] + result['status'] = 'success' if result['all_valid'] else 'validation_failed' + + # Save to file if output path specified + if args.output: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + with open(output_path, 'w') as f: + json.dump(result, f, indent=2) + + logger.info(f"Validation report saved to: {output_path}") + result['validation_report_path'] = str(output_path) + + # Print results + print(json.dumps(result, indent=2)) + + # Exit with error code if validation failed + sys.exit(0 if result['all_valid'] else 1) + + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in artifact_types parameter: {e}") + print(json.dumps({ + 'ok': False, + 'status': 'error', + 'error': f'Invalid JSON: {str(e)}' + }, indent=2)) + sys.exit(1) + + except FileNotFoundError as e: + logger.error(str(e)) + print(json.dumps({ + 'ok': False, + 'status': 'error', + 'error': str(e) + }, indent=2)) + sys.exit(1) + + except Exception as e: + logger.error(f"Unexpected error: {e}", exc_info=True) + print(json.dumps({ + 'ok': False, + 'status': 'error', + 'error': str(e) + }, indent=2)) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/skills/artifact.validate.types/benchmark_performance.py b/skills/artifact.validate.types/benchmark_performance.py new file mode 100644 index 0000000..898088d --- /dev/null +++ b/skills/artifact.validate.types/benchmark_performance.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +Performance benchmarks for artifact.validate.types + +Tests validation performance with different numbers of artifact types +to verify <1s for 20 types and <100ms for single type claims. +""" + +import json +import subprocess +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from artifact_validate_types import validate_artifact_types + + +def benchmark_validation(artifact_types: list, iterations: int = 5) -> dict: + """Benchmark validation performance.""" + times = [] + + for _ in range(iterations): + start = time.perf_counter() + result = validate_artifact_types( + artifact_types=artifact_types, + check_schemas=False, # Skip filesystem checks for pure validation speed + suggest_alternatives=True, + max_suggestions=3, + registry_path="registry/artifact_types.json" + ) + end = time.perf_counter() + times.append((end - start) * 1000) # Convert to milliseconds + + avg_time = sum(times) / len(times) + min_time = min(times) + max_time = max(times) + + return { + 'count': len(artifact_types), + 'iterations': iterations, + 'avg_ms': round(avg_time, 2), + 'min_ms': round(min_time, 2), + 'max_ms': round(max_time, 2), + 'times_ms': [round(t, 2) for t in times] + } + + +def main(): + """Run performance benchmarks.""" + print("=== artifact.validate.types Performance Benchmarks ===\n") + + # Load actual artifact types from registry + with open("registry/artifact_types.json") as f: + registry = json.load(f) + all_types = [a['name'] for a in registry['artifact_types']] + + print(f"Registry contains {len(all_types)} artifact types\n") + + # Benchmark different scenarios + scenarios = [ + ("Single type", [all_types[0]]), + ("5 types", all_types[:5]), + ("10 types", all_types[:10]), + ("20 types", all_types[:20]), + ("50 types", all_types[:50]), + ("100 types", all_types[:100]), + ("All types (409)", all_types), + ] + + results = [] + + for name, types in scenarios: + print(f"Benchmarking: {name} ({len(types)} types)") + result = benchmark_validation(types, iterations=5) + results.append({'name': name, **result}) + + avg = result['avg_ms'] + status = "✅ " if avg < 1000 else "⚠️ " + print(f" {status}Average: {avg}ms (min: {result['min_ms']}ms, max: {result['max_ms']}ms)") + print() + + # Print summary + print("\n=== Summary ===\n") + + # Check claims + single_result = results[0] + twenty_result = results[3] + + print("Claim Verification:") + if single_result['avg_ms'] < 100: + print(f" ✅ Single type < 100ms: {single_result['avg_ms']}ms") + else: + print(f" ❌ Single type < 100ms: {single_result['avg_ms']}ms (MISSED TARGET)") + + if twenty_result['avg_ms'] < 1000: + print(f" ✅ 20 types < 1s: {twenty_result['avg_ms']}ms") + else: + print(f" ❌ 20 types < 1s: {twenty_result['avg_ms']}ms (MISSED TARGET)") + + all_result = results[-1] + print(f"\nAll 409 types: {all_result['avg_ms']}ms") + + # Calculate throughput + throughput = len(all_types) / (all_result['avg_ms'] / 1000) + print(f"Throughput: {int(throughput)} types/second") + + # Save results + output_path = "skills/artifact.validate.types/benchmark_results.json" + with open(output_path, 'w') as f: + json.dump({ + 'benchmark_date': time.strftime('%Y-%m-%d %H:%M:%S'), + 'registry_size': len(all_types), + 'results': results, + 'claims_verified': { + 'single_type_under_100ms': single_result['avg_ms'] < 100, + 'twenty_types_under_1s': twenty_result['avg_ms'] < 1000 + } + }, f, indent=2) + + print(f"\nResults saved to: {output_path}") + + +if __name__ == '__main__': + main() diff --git a/skills/artifact.validate.types/benchmark_results.json b/skills/artifact.validate.types/benchmark_results.json new file mode 100644 index 0000000..910086c --- /dev/null +++ b/skills/artifact.validate.types/benchmark_results.json @@ -0,0 +1,115 @@ +{ + "benchmark_date": "2025-11-02 18:35:38", + "registry_size": 409, + "results": [ + { + "name": "Single type", + "count": 1, + "iterations": 5, + "avg_ms": 1.02, + "min_ms": 0.92, + "max_ms": 1.27, + "times_ms": [ + 1.27, + 0.95, + 1.04, + 0.93, + 0.92 + ] + }, + { + "name": "5 types", + "count": 5, + "iterations": 5, + "avg_ms": 0.98, + "min_ms": 0.89, + "max_ms": 1.05, + "times_ms": [ + 1.05, + 1.01, + 0.92, + 1.02, + 0.89 + ] + }, + { + "name": "10 types", + "count": 10, + "iterations": 5, + "avg_ms": 0.96, + "min_ms": 0.91, + "max_ms": 1.02, + "times_ms": [ + 1.02, + 0.96, + 0.95, + 0.91, + 0.98 + ] + }, + { + "name": "20 types", + "count": 20, + "iterations": 5, + "avg_ms": 1.22, + "min_ms": 1.15, + "max_ms": 1.34, + "times_ms": [ + 1.15, + 1.19, + 1.34, + 1.23, + 1.17 + ] + }, + { + "name": "50 types", + "count": 50, + "iterations": 5, + "avg_ms": 1.68, + "min_ms": 1.63, + "max_ms": 1.76, + "times_ms": [ + 1.7, + 1.76, + 1.65, + 1.63, + 1.66 + ] + }, + { + "name": "100 types", + "count": 100, + "iterations": 5, + "avg_ms": 2.73, + "min_ms": 2.49, + "max_ms": 3.54, + "times_ms": [ + 2.5, + 2.49, + 2.53, + 2.58, + 3.54 + ] + }, + { + "name": "All types (409)", + "count": 409, + "iterations": 5, + "avg_ms": 7.79, + "min_ms": 7.44, + "max_ms": 8.43, + "times_ms": [ + 7.56, + 8.43, + 7.69, + 7.44, + 7.84 + ] + } + ], + "claims_verified": { + "single_type_under_100ms": true, + "twenty_types_under_1s": true + } +} \ No newline at end of file diff --git a/skills/artifact.validate.types/skill.yaml b/skills/artifact.validate.types/skill.yaml new file mode 100644 index 0000000..321958b --- /dev/null +++ b/skills/artifact.validate.types/skill.yaml @@ -0,0 +1,84 @@ +name: artifact.validate.types +version: 0.1.0 +description: > + Validate that artifact types exist in the Betty Framework registry and return + complete metadata for each type. Provides fuzzy matching and suggestions for + invalid types using singular/plural detection and Levenshtein distance. + +inputs: + - name: artifact_types + type: array + required: true + description: List of artifact type names to validate (e.g., ["threat-model", "architecture-overview"]) + + - name: check_schemas + type: boolean + required: false + default: true + description: Whether to verify schema files exist on filesystem + + - name: suggest_alternatives + type: boolean + required: false + default: true + description: Whether to suggest similar types for invalid ones + + - name: max_suggestions + type: number + required: false + default: 3 + description: Maximum number of suggestions per invalid type + +outputs: + - name: validation_results + type: object + description: Validation results for each artifact type with complete metadata + + - name: all_valid + type: boolean + description: Whether all artifact types are valid + + - name: invalid_types + type: array + description: List of artifact types that don't exist in registry + + - name: suggestions + type: object + description: Suggested alternatives for each invalid type (type name → suggestions) + + - name: warnings + type: array + description: List of warnings (e.g., schema file missing) + +artifact_metadata: + produces: + - type: validation-report + file_pattern: "*.validation.json" + content_type: application/json + schema: schemas/validation-report.json + description: Artifact type validation results with metadata and suggestions + + consumes: [] + +entrypoints: + - command: /artifact/validate/types + handler: artifact_validate_types.py + runtime: python + permissions: + - filesystem:read + +status: active + +tags: + - artifacts + - validation + - registry + - metadata + - quality + +dependencies: + - pyyaml + - jsonschema + +permissions: + - filesystem:read diff --git a/skills/artifact.validate.types/test_artifact_validate_types.py b/skills/artifact.validate.types/test_artifact_validate_types.py new file mode 100644 index 0000000..533357d --- /dev/null +++ b/skills/artifact.validate.types/test_artifact_validate_types.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +""" +Tests for artifact.validate.types skill + +Tests fuzzy matching strategies: +- Singular/plural detection +- Generic vs specific variants +- Levenshtein distance for typos +""" + +import json +import os +import sys +import tempfile +import unittest +from pathlib import Path + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent)) + +from artifact_validate_types import ( + load_artifact_registry, + find_similar_types, + validate_artifact_types +) + + +class TestArtifactValidateTypes(unittest.TestCase): + """Test suite for artifact type validation.""" + + @classmethod + def setUpClass(cls): + """Set up test fixtures.""" + cls.registry_path = "registry/artifact_types.json" + + # Load actual registry for integration tests + if os.path.exists(cls.registry_path): + cls.artifact_types_dict = load_artifact_registry(cls.registry_path) + cls.all_type_names = list(cls.artifact_types_dict.keys()) + else: + cls.artifact_types_dict = {} + cls.all_type_names = [] + + def test_load_registry(self): + """Test loading artifact registry.""" + if not os.path.exists(self.registry_path): + self.skipTest("Registry file not found") + + registry = load_artifact_registry(self.registry_path) + + self.assertIsInstance(registry, dict) + self.assertGreater(len(registry), 0, "Registry should contain artifact types") + + # Check structure of first entry + first_type = next(iter(registry.values())) + self.assertIn('name', first_type) + self.assertIn('file_pattern', first_type) + + def test_validate_single_valid_type(self): + """Test validation of a single valid artifact type.""" + if not self.all_type_names: + self.skipTest("Registry not available") + + # Use a known artifact type (threat-model exists in registry) + result = validate_artifact_types( + artifact_types=["threat-model"], + check_schemas=False, # Don't check schema files in tests + suggest_alternatives=False, + registry_path=self.registry_path + ) + + self.assertTrue(result['all_valid']) + self.assertEqual(len(result['invalid_types']), 0) + self.assertIn('threat-model', result['validation_results']) + self.assertTrue(result['validation_results']['threat-model']['valid']) + self.assertIn('file_pattern', result['validation_results']['threat-model']) + + def test_validate_invalid_type(self): + """Test validation of an invalid artifact type.""" + result = validate_artifact_types( + artifact_types=["nonexistent-artifact-type"], + check_schemas=False, + suggest_alternatives=False, + registry_path=self.registry_path + ) + + self.assertFalse(result['all_valid']) + self.assertIn('nonexistent-artifact-type', result['invalid_types']) + self.assertFalse( + result['validation_results']['nonexistent-artifact-type']['valid'] + ) + + def test_validate_mixed_types(self): + """Test validation of mix of valid and invalid types.""" + if not self.all_type_names: + self.skipTest("Registry not available") + + result = validate_artifact_types( + artifact_types=["threat-model", "invalid-type", "architecture-overview"], + check_schemas=False, + suggest_alternatives=False, + registry_path=self.registry_path + ) + + self.assertFalse(result['all_valid']) + self.assertEqual(len(result['invalid_types']), 1) + self.assertIn('invalid-type', result['invalid_types']) + + # Valid types should have metadata + self.assertTrue(result['validation_results']['threat-model']['valid']) + self.assertTrue(result['validation_results']['architecture-overview']['valid']) + + def test_singular_plural_suggestion(self): + """Test singular/plural fuzzy matching.""" + # Create test data + all_types = ["data-flow-diagrams", "threat-model", "api-spec"] + + # Test plural → singular suggestion + suggestions = find_similar_types("data-flow-diagram", all_types, max_suggestions=3) + + # Should suggest plural form + self.assertTrue(any( + s['type'] == 'data-flow-diagrams' and s['confidence'] == 'high' + for s in suggestions + )) + + def test_generic_specific_suggestion(self): + """Test generic vs specific variant matching.""" + # Create test data with specific variants + all_types = [ + "logical-data-model", + "physical-data-model", + "enterprise-data-model", + "threat-model" + ] + + # Search for generic "data-model" + suggestions = find_similar_types("data-model", all_types, max_suggestions=3) + + # Should suggest specific variants ending in "-data-model" + suggested_types = [s['type'] for s in suggestions] + self.assertTrue( + any('data-model' in t for t in suggested_types), + "Should suggest specific data-model variants" + ) + + def test_typo_suggestion(self): + """Test Levenshtein distance for typo detection.""" + all_types = ["threat-model", "architecture-overview", "api-specification"] + + # Typo: "thret-model" instead of "threat-model" + suggestions = find_similar_types("thret-model", all_types, max_suggestions=3) + + # Should suggest "threat-model" + suggested_types = [s['type'] for s in suggestions] + self.assertIn("threat-model", suggested_types) + + def test_max_suggestions_limit(self): + """Test that max_suggestions limit is respected.""" + all_types = [ + "logical-data-model", + "physical-data-model", + "enterprise-data-model", + "conceptual-data-model", + "canonical-data-model" + ] + + suggestions = find_similar_types("data-model", all_types, max_suggestions=2) + + # Should return at most 2 suggestions + self.assertLessEqual(len(suggestions), 2) + + def test_suggestions_integration(self): + """Test end-to-end suggestion workflow.""" + if not self.all_type_names: + self.skipTest("Registry not available") + + result = validate_artifact_types( + artifact_types=["data-flow-diagram"], # Singular (likely plural in registry) + check_schemas=False, + suggest_alternatives=True, + max_suggestions=3, + registry_path=self.registry_path + ) + + # Should detect as invalid + self.assertFalse(result['all_valid']) + self.assertIn('data-flow-diagram', result['invalid_types']) + + # Should provide suggestions + if 'data-flow-diagram' in result['suggestions']: + suggestions = result['suggestions']['data-flow-diagram'] + self.assertGreater(len(suggestions), 0) + + # Check suggestion structure + first_suggestion = suggestions[0] + self.assertIn('type', first_suggestion) + self.assertIn('reason', first_suggestion) + self.assertIn('confidence', first_suggestion) + + def test_schema_checking(self): + """Test schema file existence checking.""" + if not os.path.exists(self.registry_path): + self.skipTest("Registry not available") + + # Validate a type that has a schema + result = validate_artifact_types( + artifact_types=["threat-model"], + check_schemas=True, + suggest_alternatives=False, + registry_path=self.registry_path + ) + + # If schema is missing, should have warning + # If schema exists, no warning + if 'schemas/artifacts/threat-model-schema.json' not in [ + w for w in result['warnings'] if 'threat-model' in w + ]: + # Schema exists - good + pass + else: + # Schema missing - warning should be present + self.assertTrue(any('threat-model' in w for w in result['warnings'])) + + def test_empty_input(self): + """Test validation with empty artifact types list.""" + result = validate_artifact_types( + artifact_types=[], + check_schemas=False, + suggest_alternatives=False, + registry_path=self.registry_path + ) + + self.assertTrue(result['all_valid']) + self.assertEqual(len(result['invalid_types']), 0) + self.assertEqual(len(result['validation_results']), 0) + + def test_validation_report_structure(self): + """Test that validation report has correct structure.""" + result = validate_artifact_types( + artifact_types=["threat-model", "invalid-type"], + check_schemas=False, + suggest_alternatives=True, + max_suggestions=3, + registry_path=self.registry_path + ) + + # Check required fields + self.assertIn('validation_results', result) + self.assertIn('all_valid', result) + self.assertIn('invalid_types', result) + self.assertIn('suggestions', result) + self.assertIn('warnings', result) + + # Check types + self.assertIsInstance(result['validation_results'], dict) + self.assertIsInstance(result['all_valid'], bool) + self.assertIsInstance(result['invalid_types'], list) + self.assertIsInstance(result['suggestions'], dict) + self.assertIsInstance(result['warnings'], list) + + +class TestFuzzMatchingStrategies(unittest.TestCase): + """Focused tests for fuzzy matching strategies.""" + + def test_plural_to_singular(self): + """Test plural → singular suggestion.""" + all_types = ["threat-model", "data-flow-diagram"] + suggestions = find_similar_types("threat-models", all_types) + + # Should suggest singular + self.assertTrue(any( + s['type'] == 'threat-model' and + s['reason'] == 'Singular form' and + s['confidence'] == 'high' + for s in suggestions + )) + + def test_singular_to_plural(self): + """Test singular → plural suggestion.""" + all_types = ["data-flow-diagrams", "threat-models"] + suggestions = find_similar_types("threat-model", all_types) + + # Should suggest plural + self.assertTrue(any( + s['type'] == 'threat-models' and + s['reason'] == 'Plural form' and + s['confidence'] == 'high' + for s in suggestions + )) + + def test_generic_to_specific(self): + """Test generic → specific variant suggestions.""" + all_types = [ + "openapi-spec", + "asyncapi-spec", + "graphql-spec" + ] + + suggestions = find_similar_types("api-spec", all_types) + + # Should suggest specific API spec variants + suggested_types = [s['type'] for s in suggestions] + self.assertTrue(any('spec' in t for t in suggested_types)) + + def test_confidence_levels(self): + """Test that confidence levels are correctly assigned.""" + all_types = [ + "threat-model", # For singular/plural (high confidence) + "logical-data-model", # For specific variant (medium confidence) + "thret-model" # For typo (low confidence - but this is exact match) + ] + + # Plural → singular (high) + suggestions = find_similar_types("threat-models", all_types) + high_conf = [s for s in suggestions if s['confidence'] == 'high'] + self.assertTrue(len(high_conf) > 0) + + +def run_tests(): + """Run all tests.""" + unittest.main(argv=[''], verbosity=2, exit=False) + + +if __name__ == '__main__': + run_tests() diff --git a/skills/artifact.validate/README.md b/skills/artifact.validate/README.md new file mode 100644 index 0000000..4aad06f --- /dev/null +++ b/skills/artifact.validate/README.md @@ -0,0 +1,315 @@ +# artifact.validate + +Automated artifact validation against structure, schema, and quality criteria. + +## Purpose + +The `artifact.validate` skill provides comprehensive validation of artifacts to ensure: +- Correct syntax (YAML/Markdown) +- Complete metadata +- Required sections present +- No placeholder content +- Schema compliance (when applicable) +- Quality standards met + +## Features + +✅ **Syntax Validation**: YAML and Markdown format checking +✅ **Metadata Validation**: Document control completeness +✅ **Section Validation**: Required sections verification +✅ **TODO Detection**: Placeholder and incomplete content identification +✅ **Schema Validation**: JSON schema compliance (when provided) +✅ **Quality Scoring**: 0-100 quality score with breakdown +✅ **Strict Mode**: Enforce all recommendations +✅ **Detailed Reporting**: Actionable validation reports + +## Usage + +### Basic Validation + +```bash +python3 skills/artifact.validate/artifact_validate.py +``` + +### With Artifact Type + +```bash +python3 skills/artifact.validate/artifact_validate.py \ + my-artifact.yaml \ + --artifact-type business-case +``` + +### Strict Mode + +```bash +python3 skills/artifact.validate/artifact_validate.py \ + my-artifact.yaml \ + --strict +``` + +Strict mode treats warnings as errors. Useful for: +- CI/CD pipeline gates +- Approval workflow requirements +- Production release criteria + +### With JSON Schema + +```bash +python3 skills/artifact.validate/artifact_validate.py \ + my-business-case.yaml \ + --schema-path schemas/artifacts/business-case-schema.json +``` + +### Save Validation Report + +```bash +python3 skills/artifact.validate/artifact_validate.py \ + my-artifact.yaml \ + --output validation-report.yaml +``` + +## Validation Checks + +### 1. Syntax Validation (30% weight) + +**YAML Artifacts**: +- Valid YAML syntax +- Proper indentation +- No duplicate keys +- Valid data types + +**Markdown Artifacts**: +- At least one heading +- Document control section +- Proper markdown structure + +**Score**: +- ✅ Valid: 100 points +- ❌ Invalid: 0 points + +### 2. Metadata Completeness (25% weight) + +**Required Fields**: +- `version` - Semantic version (e.g., 1.0.0) +- `created` - Creation date (YYYY-MM-DD) +- `author` - Author name or team +- `status` - Draft | Review | Approved | Published + +**Recommended Fields**: +- `lastModified` - Last modification date +- `classification` - Public | Internal | Confidential | Restricted +- `documentOwner` - Owning role or person +- `approvers` - Approval workflow +- `relatedDocuments` - Dependencies and references + +**Scoring**: +- Missing required field: -25 points each +- Missing recommended field: -10 points each +- TODO placeholder in field: -10 points + +### 3. Required Sections (25% weight) + +**YAML Artifacts**: +- `metadata` section required +- `content` section expected (unless schema-only artifact) +- Empty content fields detected + +**Scoring**: +- Missing required section: -30 points each +- Empty content warning: -15 points each + +### 4. TODO Markers (20% weight) + +Detects placeholder content: +- `TODO` markers +- `TODO:` comments +- Empty required fields +- Template placeholders + +**Scoring**: +- Each TODO marker: -5 points +- Score floor: 0 (cannot go negative) + +## Quality Score Interpretation + +| Score | Rating | Meaning | Recommendation | +|-------|--------|---------|----------------| +| 90-100 | Excellent | Ready for approval | Minimal polish | +| 70-89 | Good | Minor improvements | Review recommendations | +| 50-69 | Fair | Needs refinement | Address key issues | +| < 50 | Poor | Significant work needed | Major revision required | + +## Validation Report Structure + +```yaml +success: true +validation_results: + artifact_path: /path/to/artifact.yaml + artifact_type: business-case + file_format: yaml + file_size: 2351 + validated_at: 2025-10-25T19:30:00 + + syntax: + valid: true + error: null + + metadata: + complete: false + score: 90 + issues: [] + warnings: + - "Field 'documentOwner' contains TODO marker" + + sections: + valid: true + score: 100 + issues: [] + warnings: [] + + todos: + - "Line 10: author: TODO" + - "Line 15: documentOwner: TODO" + # ... more TODOs + + quality_score: 84 + recommendations: + - "🟡 Complete 1 recommended metadata field(s)" + - "🔴 Replace 13 TODO markers with actual content" + - "🟢 Artifact is good - minor improvements recommended" + +is_valid: true +quality_score: 84 +``` + +## Command-Line Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `artifact_path` | string | required | Path to artifact file | +| `--artifact-type` | string | auto-detect | Artifact type override | +| `--strict` | flag | false | Treat warnings as errors | +| `--schema-path` | string | none | Path to JSON schema | +| `--output` | string | none | Save report to file | + +## Exit Codes + +- `0`: Validation passed (artifact is valid) +- `1`: Validation failed (artifact has critical issues) + +In strict mode, warnings also cause exit code `1`. + +## Integration Examples + +### CI/CD Pipeline (GitHub Actions) + +```yaml +name: Validate Artifacts + +on: [push, pull_request] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Validate artifacts + run: | + python3 skills/artifact.validate/artifact_validate.py \ + artifacts/*.yaml \ + --strict +``` + +### Pre-commit Hook + +```bash +#!/bin/bash +# .git/hooks/pre-commit + +# Validate all modified YAML artifacts +git diff --cached --name-only | grep '\.yaml$' | while read file; do + python3 skills/artifact.validate/artifact_validate.py "$file" --strict + if [ $? -ne 0 ]; then + echo "❌ Validation failed for $file" + exit 1 + fi +done +``` + +### Makefile Target + +```makefile +.PHONY: validate +validate: + @echo "Validating artifacts..." + @find artifacts -name "*.yaml" -exec \ + python3 skills/artifact.validate/artifact_validate.py {} \; +``` + +## Artifact Type Detection + +The skill automatically detects artifact type using: + +1. **Filename match**: Direct match with registry (e.g., `business-case.yaml`) +2. **Partial match**: Artifact type found in filename (e.g., `portal-business-case.yaml` → `business-case`) +3. **Metadata**: `artifactType` field in YAML metadata +4. **Manual override**: `--artifact-type` parameter + +## Error Handling + +### File Not Found +``` +Error: Artifact file not found: /path/to/artifact.yaml +``` + +### Unsupported Format +``` +Error: Unsupported file format: .txt. Expected yaml, yml, or md +``` + +### YAML Syntax Error +``` +Syntax Validation: + ❌ YAML syntax error: while parsing a block mapping + in "", line 3, column 1 + expected , but found '' +``` + +## Performance + +- **Validation time**: < 100ms per artifact +- **Memory usage**: < 10MB +- **Scalability**: Can validate 1000+ artifacts in batch + +## Dependencies + +- Python 3.7+ +- `yaml` (PyYAML) - YAML parsing +- `artifact.define` skill - Artifact registry + +## Status + +**Active** - Phase 2 implementation complete + +## Tags + +artifacts, validation, quality, schema, tier2, phase2 + +## Version History + +- **0.1.0** (2025-10-25): Initial implementation + - Syntax validation (YAML/Markdown) + - Metadata completeness checking + - Required section verification + - TODO marker detection + - Quality scoring + - Strict mode + - JSON schema support (framework) + +## See Also + +- `artifact.review` - AI-powered content quality review +- `artifact.create` - Generate artifacts from templates +- `schemas/artifacts/` - JSON schema library +- `docs/ARTIFACT_USAGE_GUIDE.md` - Complete usage guide diff --git a/skills/artifact.validate/__init__.py b/skills/artifact.validate/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/artifact.validate/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/artifact.validate/artifact_validate.py b/skills/artifact.validate/artifact_validate.py new file mode 100755 index 0000000..233284e --- /dev/null +++ b/skills/artifact.validate/artifact_validate.py @@ -0,0 +1,598 @@ +#!/usr/bin/env python3 +""" +artifact.validate skill - Comprehensive artifact validation + +Validates artifacts against structure, schema, and quality criteria. +Generates detailed validation reports with scores and recommendations. +""" + +import sys +import os +import argparse +import re +from pathlib import Path +from datetime import datetime +from typing import Dict, Any, List, Optional, Tuple +import yaml +import json + + +def load_artifact_registry() -> Dict[str, Any]: + """Load artifact registry from artifact.define skill""" + registry_file = Path(__file__).parent.parent / "artifact.define" / "artifact_define.py" + + if not registry_file.exists(): + raise FileNotFoundError(f"Artifact registry not found: {registry_file}") + + with open(registry_file, 'r') as f: + content = f.read() + + # Find KNOWN_ARTIFACT_TYPES dictionary + start_marker = "KNOWN_ARTIFACT_TYPES = {" + start_idx = content.find(start_marker) + if start_idx == -1: + raise ValueError("Could not find KNOWN_ARTIFACT_TYPES in registry file") + + start_idx += len(start_marker) - 1 + + # Find matching closing brace + brace_count = 0 + end_idx = start_idx + for i in range(start_idx, len(content)): + if content[i] == '{': + brace_count += 1 + elif content[i] == '}': + brace_count -= 1 + if brace_count == 0: + end_idx = i + 1 + break + + dict_str = content[start_idx:end_idx] + artifacts = eval(dict_str) + return artifacts + + +def detect_artifact_type(file_path: Path, content: str) -> Optional[str]: + """Detect artifact type from filename or content""" + # Try to detect from filename + filename = file_path.stem + + # Load registry to check against known types + registry = load_artifact_registry() + + # Direct match + if filename in registry: + return filename + + # Check for partial matches + for artifact_type in registry.keys(): + if artifact_type in filename: + return artifact_type + + # Try to detect from content (YAML metadata) + if file_path.suffix in ['.yaml', '.yml']: + try: + data = yaml.safe_load(content) + if isinstance(data, dict) and 'metadata' in data: + # Check for artifact type in metadata + metadata = data['metadata'] + if 'artifactType' in metadata: + return metadata['artifactType'] + except: + pass + + return None + + +def validate_yaml_syntax(content: str) -> Tuple[bool, Optional[str]]: + """Validate YAML syntax""" + try: + yaml.safe_load(content) + return True, None + except yaml.YAMLError as e: + return False, f"YAML syntax error: {str(e)}" + + +def validate_markdown_structure(content: str) -> Tuple[bool, List[str]]: + """Validate Markdown structure""" + issues = [] + + # Check for at least one heading + if not re.search(r'^#+ ', content, re.MULTILINE): + issues.append("No headings found - document should have structured sections") + + # Check for document control section + if 'Document Control' not in content and 'Metadata' not in content: + issues.append("Missing document control/metadata section") + + return len(issues) == 0, issues + + +def check_metadata_completeness(data: Dict[str, Any], file_format: str) -> Dict[str, Any]: + """Check metadata section completeness""" + issues = [] + warnings = [] + metadata = {} + + if file_format in ['yaml', 'yml']: + if 'metadata' not in data: + issues.append("Missing 'metadata' section") + return { + 'complete': False, + 'score': 0, + 'issues': issues, + 'warnings': warnings + } + + metadata = data['metadata'] + + # Required fields + required = ['version', 'created', 'author', 'status'] + for field in required: + if field not in metadata or not metadata[field] or metadata[field] == 'TODO': + issues.append(f"Missing or incomplete required field: {field}") + + # Recommended fields + recommended = ['lastModified', 'classification', 'documentOwner'] + for field in recommended: + if field not in metadata or not metadata[field]: + warnings.append(f"Missing recommended field: {field}") + + # Check for TODO placeholders + if isinstance(metadata, dict): + for key, value in metadata.items(): + if isinstance(value, str) and 'TODO' in value: + warnings.append(f"Field '{key}' contains TODO marker: {value}") + + score = max(0, 100 - (len(issues) * 25) - (len(warnings) * 10)) + + return { + 'complete': len(issues) == 0, + 'score': score, + 'issues': issues, + 'warnings': warnings, + 'metadata': metadata + } + + +def count_todo_markers(content: str) -> List[str]: + """Count and locate TODO markers in content""" + todos = [] + lines = content.split('\n') + + for i, line in enumerate(lines, 1): + if 'TODO' in line: + # Extract the TODO text + todo_text = line.strip() + if len(todo_text) > 100: + todo_text = todo_text[:100] + "..." + todos.append(f"Line {i}: {todo_text}") + + return todos + + +def validate_required_sections(data: Dict[str, Any], artifact_type: str, file_format: str) -> Dict[str, Any]: + """Validate that required sections are present""" + issues = [] + warnings = [] + + if file_format in ['yaml', 'yml']: + # Common required sections for YAML artifacts + if 'metadata' not in data: + issues.append("Missing 'metadata' section") + + if 'content' not in data and artifact_type not in ['schema-definition', 'data-model']: + warnings.append("Missing 'content' section - artifact may be incomplete") + + # Check for empty content + if 'content' in data: + content = data['content'] + if isinstance(content, dict): + empty_fields = [k for k, v in content.items() if not v or (isinstance(v, str) and v.strip() == 'TODO: ')] + if empty_fields: + warnings.append(f"Empty content fields: {', '.join(empty_fields)}") + + score = max(0, 100 - (len(issues) * 30) - (len(warnings) * 15)) + + return { + 'valid': len(issues) == 0, + 'score': score, + 'issues': issues, + 'warnings': warnings + } + + +def validate_against_schema(data: Dict[str, Any], schema_path: Optional[Path]) -> Dict[str, Any]: + """Validate artifact against JSON schema if available""" + if not schema_path or not schema_path.exists(): + return { + 'validated': False, + 'score': None, + 'message': 'No schema available for validation' + } + + try: + with open(schema_path, 'r') as f: + schema = json.load(f) + + # Note: Would need jsonschema library for full validation + # For now, just indicate schema was found + return { + 'validated': False, + 'score': None, + 'message': 'Schema validation not yet implemented (requires jsonschema library)' + } + except Exception as e: + return { + 'validated': False, + 'score': None, + 'message': f'Schema validation error: {str(e)}' + } + + +def calculate_quality_score(validation_results: Dict[str, Any]) -> int: + """Calculate overall quality score from validation results""" + scores = [] + weights = [] + + # Syntax validation (weight: 30%) + if validation_results['syntax']['valid']: + scores.append(100) + else: + scores.append(0) + weights.append(0.30) + + # Metadata completeness (weight: 25%) + scores.append(validation_results['metadata']['score']) + weights.append(0.25) + + # Required sections (weight: 25%) + scores.append(validation_results['sections']['score']) + weights.append(0.25) + + # TODO markers - penalty (weight: 20%) + todo_count = len(validation_results['todos']) + todo_score = max(0, 100 - (todo_count * 5)) + scores.append(todo_score) + weights.append(0.20) + + # Calculate weighted average + quality_score = sum(s * w for s, w in zip(scores, weights)) + + return int(quality_score) + + +def generate_recommendations(validation_results: Dict[str, Any]) -> List[str]: + """Generate actionable recommendations based on validation results""" + recommendations = [] + + # Syntax issues + if not validation_results['syntax']['valid']: + recommendations.append("🔴 CRITICAL: Fix syntax errors before proceeding") + + # Metadata issues + metadata = validation_results['metadata'] + if metadata['issues']: + recommendations.append(f"🔴 Fix {len(metadata['issues'])} required metadata field(s)") + if metadata['warnings']: + recommendations.append(f"🟡 Complete {len(metadata['warnings'])} recommended metadata field(s)") + + # Section issues + sections = validation_results['sections'] + if sections['issues']: + recommendations.append(f"🔴 Add {len(sections['issues'])} required section(s)") + if sections['warnings']: + recommendations.append(f"🟡 Review {len(sections['warnings'])} section warning(s)") + + # TODO markers + todo_count = len(validation_results['todos']) + if todo_count > 0: + if todo_count > 10: + recommendations.append(f"🔴 Replace {todo_count} TODO markers with actual content") + else: + recommendations.append(f"🟡 Replace {todo_count} TODO marker(s) with actual content") + + # Quality score recommendations + quality_score = validation_results['quality_score'] + if quality_score < 50: + recommendations.append("🔴 Artifact needs significant work before it's ready for review") + elif quality_score < 70: + recommendations.append("🟡 Artifact needs refinement before it's ready for approval") + elif quality_score < 90: + recommendations.append("🟢 Artifact is good - minor improvements recommended") + else: + recommendations.append("✅ Artifact meets quality standards") + + return recommendations + + +def validate_artifact( + artifact_path: str, + artifact_type: Optional[str] = None, + strict: bool = False, + schema_path: Optional[str] = None +) -> Dict[str, Any]: + """ + Validate an artifact against structure, schema, and quality criteria + + Args: + artifact_path: Path to artifact file + artifact_type: Type of artifact (auto-detected if not provided) + strict: Strict mode - treat warnings as errors + schema_path: Optional path to JSON schema + + Returns: + Validation report with scores and recommendations + """ + file_path = Path(artifact_path) + + # Check file exists + if not file_path.exists(): + return { + 'success': False, + 'error': f"Artifact file not found: {artifact_path}", + 'is_valid': False, + 'quality_score': 0 + } + + # Read file + with open(file_path, 'r') as f: + content = f.read() + + # Detect format + file_format = file_path.suffix.lstrip('.') + if file_format not in ['yaml', 'yml', 'md']: + return { + 'success': False, + 'error': f"Unsupported file format: {file_format}. Expected yaml, yml, or md", + 'is_valid': False, + 'quality_score': 0 + } + + # Detect or validate artifact type + detected_type = detect_artifact_type(file_path, content) + if artifact_type and detected_type and artifact_type != detected_type: + print(f"Warning: Specified type '{artifact_type}' differs from detected type '{detected_type}'") + + final_type = artifact_type or detected_type or "unknown" + + # Initialize validation results + validation_results = { + 'artifact_path': str(file_path.absolute()), + 'artifact_type': final_type, + 'file_format': file_format, + 'file_size': len(content), + 'validated_at': datetime.now().isoformat() + } + + # 1. Syntax validation + if file_format in ['yaml', 'yml']: + is_valid, error = validate_yaml_syntax(content) + validation_results['syntax'] = { + 'valid': is_valid, + 'error': error + } + + # Parse for further validation + if is_valid: + data = yaml.safe_load(content) + else: + # Cannot continue without valid syntax + return { + 'success': True, + 'validation_results': validation_results, + 'is_valid': False, + 'quality_score': 0, + 'recommendations': ['🔴 CRITICAL: Fix YAML syntax errors before proceeding'] + } + else: # Markdown + is_valid, issues = validate_markdown_structure(content) + validation_results['syntax'] = { + 'valid': is_valid, + 'issues': issues if not is_valid else [] + } + data = {} # Markdown doesn't parse to structured data + + # 2. Metadata completeness + if file_format in ['yaml', 'yml']: + validation_results['metadata'] = check_metadata_completeness(data, file_format) + else: + validation_results['metadata'] = { + 'complete': True, + 'score': 100, + 'issues': [], + 'warnings': [] + } + + # 3. TODO markers + validation_results['todos'] = count_todo_markers(content) + + # 4. Required sections + if file_format in ['yaml', 'yml']: + validation_results['sections'] = validate_required_sections(data, final_type, file_format) + else: + validation_results['sections'] = { + 'valid': True, + 'score': 100, + 'issues': [], + 'warnings': [] + } + + # 5. Schema validation (if schema provided) + if schema_path: + validation_results['schema'] = validate_against_schema(data, Path(schema_path)) + + # Calculate quality score + quality_score = calculate_quality_score(validation_results) + validation_results['quality_score'] = quality_score + + # Generate recommendations + recommendations = generate_recommendations(validation_results) + validation_results['recommendations'] = recommendations + + # Determine overall validity + has_critical_issues = ( + not validation_results['syntax']['valid'] or + len(validation_results['metadata']['issues']) > 0 or + len(validation_results['sections']['issues']) > 0 + ) + + has_warnings = ( + len(validation_results['metadata']['warnings']) > 0 or + len(validation_results['sections']['warnings']) > 0 or + len(validation_results['todos']) > 0 + ) + + if strict: + is_valid = not has_critical_issues and not has_warnings + else: + is_valid = not has_critical_issues + + return { + 'success': True, + 'validation_results': validation_results, + 'is_valid': is_valid, + 'quality_score': quality_score, + 'recommendations': recommendations + } + + +def main(): + """Main entry point for artifact.validate skill""" + parser = argparse.ArgumentParser( + description='Validate artifacts against structure, schema, and quality criteria' + ) + parser.add_argument( + 'artifact_path', + type=str, + help='Path to artifact file to validate' + ) + parser.add_argument( + '--artifact-type', + type=str, + help='Type of artifact (auto-detected if not provided)' + ) + parser.add_argument( + '--strict', + action='store_true', + help='Strict mode - treat warnings as errors' + ) + parser.add_argument( + '--schema-path', + type=str, + help='Path to JSON schema for validation' + ) + parser.add_argument( + '--output', + type=str, + help='Save validation report to file' + ) + + args = parser.parse_args() + + # Validate artifact + result = validate_artifact( + artifact_path=args.artifact_path, + artifact_type=args.artifact_type, + strict=args.strict, + schema_path=args.schema_path + ) + + # Save to file if requested + if args.output: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'w') as f: + yaml.dump(result, f, default_flow_style=False, sort_keys=False) + print(f"\nValidation report saved to: {output_path}") + + # Print report + if not result['success']: + print(f"\n{'='*70}") + print(f"✗ Validation Failed") + print(f"{'='*70}") + print(f"Error: {result['error']}") + print(f"{'='*70}\n") + return 1 + + vr = result['validation_results'] + + print(f"\n{'='*70}") + print(f"Artifact Validation Report") + print(f"{'='*70}") + print(f"Artifact: {vr['artifact_path']}") + print(f"Type: {vr['artifact_type']}") + print(f"Format: {vr['file_format']}") + print(f"Size: {vr['file_size']} bytes") + print(f"") + print(f"Overall Status: {'✅ VALID' if result['is_valid'] else '❌ INVALID'}") + print(f"Quality Score: {result['quality_score']}/100") + print(f"") + + # Syntax + print(f"Syntax Validation:") + if vr['syntax']['valid']: + print(f" ✅ Valid {vr['file_format'].upper()} syntax") + else: + print(f" ❌ {vr['syntax'].get('error', 'Syntax errors found')}") + if 'issues' in vr['syntax']: + for issue in vr['syntax']['issues']: + print(f" - {issue}") + print() + + # Metadata + print(f"Metadata Completeness: {vr['metadata']['score']}/100") + if vr['metadata']['issues']: + print(f" Issues:") + for issue in vr['metadata']['issues']: + print(f" ❌ {issue}") + if vr['metadata']['warnings']: + print(f" Warnings:") + for warning in vr['metadata']['warnings']: + print(f" 🟡 {warning}") + if not vr['metadata']['issues'] and not vr['metadata']['warnings']: + print(f" ✅ All metadata fields complete") + print() + + # Sections + print(f"Required Sections: {vr['sections']['score']}/100") + if vr['sections']['issues']: + print(f" Issues:") + for issue in vr['sections']['issues']: + print(f" ❌ {issue}") + if vr['sections']['warnings']: + print(f" Warnings:") + for warning in vr['sections']['warnings']: + print(f" 🟡 {warning}") + if not vr['sections']['issues'] and not vr['sections']['warnings']: + print(f" ✅ All required sections present") + print() + + # TODOs + todo_count = len(vr['todos']) + print(f"TODO Markers: {todo_count}") + if todo_count > 0: + print(f" 🟡 Found {todo_count} TODO marker(s) - artifact incomplete") + if todo_count <= 5: + for todo in vr['todos']: + print(f" - {todo}") + else: + for todo in vr['todos'][:5]: + print(f" - {todo}") + print(f" ... and {todo_count - 5} more") + else: + print(f" ✅ No TODO markers found") + print() + + # Recommendations + print(f"Recommendations:") + for rec in result['recommendations']: + print(f" {rec}") + + print(f"{'='*70}\n") + + return 0 if result['is_valid'] else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/skills/artifact.validate/skill.yaml b/skills/artifact.validate/skill.yaml new file mode 100644 index 0000000..32e1f9d --- /dev/null +++ b/skills/artifact.validate/skill.yaml @@ -0,0 +1,96 @@ +name: artifact.validate +version: 0.1.0 +description: > + Validate artifacts against schema, structure, and quality criteria. Checks for + completeness, correct format, required fields, and generates detailed validation + reports with quality scores and actionable recommendations. + +inputs: + - name: artifact_path + type: string + required: true + description: Path to the artifact file to validate + + - name: artifact_type + type: string + required: false + description: Type of artifact (auto-detected from filename/content if not provided) + + - name: strict + type: boolean + required: false + default: false + description: Strict mode - fail validation on warnings + + - name: schema_path + type: string + required: false + description: Optional path to custom JSON schema for validation + +outputs: + - name: validation_report + type: object + description: Detailed validation results with scores and recommendations + + - name: is_valid + type: boolean + description: Overall validation status (true if artifact passes validation) + + - name: quality_score + type: number + description: Quality score from 0-100 based on completeness and best practices + +dependencies: + - artifact.define + +entrypoints: + - command: /skill/artifact/validate + handler: artifact_validate.py + runtime: python + description: > + Validate artifacts for structure, completeness, and quality. Performs syntax + validation (YAML/Markdown), metadata completeness checks, schema validation, + TODO marker detection, and required section verification. Generates detailed + reports with quality scores and actionable recommendations. + parameters: + - name: artifact_path + type: string + required: true + description: Path to artifact file + - name: artifact_type + type: string + required: false + description: Artifact type (auto-detected if not provided) + - name: strict + type: boolean + required: false + default: false + description: Strict mode - treat warnings as errors + - name: schema_path + type: string + required: false + description: Custom JSON schema path + permissions: + - filesystem:read + +status: active + +tags: + - artifacts + - validation + - quality + - tier2 + - phase2 + +# This skill's own artifact metadata +artifact_metadata: + produces: + - type: validation-report + description: Detailed artifact validation report with scores and recommendations + file_pattern: "*-validation-report.yaml" + content_type: application/yaml + + consumes: + - type: "*" + description: Validates any artifact type from the registry + file_pattern: "**/*.{yaml,yml,md}" diff --git a/skills/build.optimize/__init__.py b/skills/build.optimize/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/build.optimize/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/build.optimize/build_optimize.py b/skills/build.optimize/build_optimize.py new file mode 100755 index 0000000..bf17638 --- /dev/null +++ b/skills/build.optimize/build_optimize.py @@ -0,0 +1,551 @@ +#!/usr/bin/env python3 +""" +Build Optimization Skill + +Analyzes and optimizes build processes and speed across various build systems. + +Supports: +- Webpack, Vite, Rollup, esbuild +- TypeScript compilation +- Node.js build processes +- General build optimization strategies +""" + +import sys +import json +import argparse +import subprocess +from pathlib import Path +from typing import Dict, Any, List, Optional, Tuple +import re +import time + +# Add betty module to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response, BettyError +from betty.telemetry_capture import telemetry_decorator + +logger = setup_logger(__name__) + + +class BuildOptimizer: + """Comprehensive build optimization analyzer and executor""" + + def __init__(self, project_path: str): + """ + Initialize build optimizer + + Args: + project_path: Path to project root directory + """ + self.project_path = Path(project_path).resolve() + + if not self.project_path.exists(): + raise BettyError(f"Project path does not exist: {project_path}") + + if not self.project_path.is_dir(): + raise BettyError(f"Project path is not a directory: {project_path}") + + self.build_system = None + self.package_json = None + self.analysis_results = {} + self.recommendations = [] + + def analyze(self, args: str = "") -> Dict[str, Any]: + """ + Comprehensive build analysis + + Args: + args: Optional arguments for analysis + + Returns: + Dict with analysis results and recommendations + """ + logger.info(f"Starting build optimization analysis for {self.project_path}") + + results = { + "project_path": str(self.project_path), + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "build_system": None, + "analysis": {}, + "recommendations": [], + "estimated_improvement": "unknown" + } + + try: + # Step 1: Identify build system + build_system_info = self._identify_build_system() + results["build_system"] = build_system_info + logger.info(f"Build system: {build_system_info['type']}") + + # Step 2: Analyze dependencies + dep_analysis = self._analyze_dependencies() + results["analysis"]["dependencies"] = dep_analysis + + # Step 3: Analyze caching + cache_analysis = self._analyze_caching() + results["analysis"]["caching"] = cache_analysis + + # Step 4: Analyze bundle configuration + bundle_analysis = self._analyze_bundling() + results["analysis"]["bundling"] = bundle_analysis + + # Step 5: Analyze TypeScript configuration + ts_analysis = self._analyze_typescript() + results["analysis"]["typescript"] = ts_analysis + + # Step 6: Analyze parallelization + parallel_analysis = self._analyze_parallelization() + results["analysis"]["parallelization"] = parallel_analysis + + # Generate recommendations based on analysis + recommendations = self._generate_recommendations(results["analysis"]) + results["recommendations"] = recommendations + + # Estimate potential improvement + results["estimated_improvement"] = self._estimate_improvement( + results["analysis"], recommendations + ) + + logger.info(f"Analysis complete. Found {len(recommendations)} optimization opportunities") + + return results + + except Exception as e: + logger.error(f"Analysis failed: {e}") + raise BettyError(f"Build analysis failed: {e}") + + def _identify_build_system(self) -> Dict[str, Any]: + """ + Step 1: Identify the build system in use + """ + logger.info("Identifying build system...") + + package_json_path = self.project_path / "package.json" + + if not package_json_path.exists(): + return { + "type": "unknown", + "detected": False, + "message": "No package.json found" + } + + # Load package.json + with open(package_json_path, 'r') as f: + self.package_json = json.load(f) + + build_system = {"type": "unknown", "detected": True, "configs": []} + + # Check for build tools in dependencies and config files + deps = { + **self.package_json.get("dependencies", {}), + **self.package_json.get("devDependencies", {}) + } + + # Check for Vite + if "vite" in deps or (self.project_path / "vite.config.js").exists() or \ + (self.project_path / "vite.config.ts").exists(): + build_system["type"] = "vite" + build_system["configs"].append("vite.config.js/ts") + + # Check for Webpack + elif "webpack" in deps or (self.project_path / "webpack.config.js").exists(): + build_system["type"] = "webpack" + build_system["configs"].append("webpack.config.js") + + # Check for Rollup + elif "rollup" in deps or (self.project_path / "rollup.config.js").exists(): + build_system["type"] = "rollup" + build_system["configs"].append("rollup.config.js") + + # Check for esbuild + elif "esbuild" in deps: + build_system["type"] = "esbuild" + + # Check for TypeScript + elif "typescript" in deps or (self.project_path / "tsconfig.json").exists(): + build_system["type"] = "typescript" + build_system["configs"].append("tsconfig.json") + + else: + build_system["type"] = "generic" + + # Check build scripts + scripts = self.package_json.get("scripts", {}) + build_system["scripts"] = { + "build": scripts.get("build"), + "dev": scripts.get("dev"), + "test": scripts.get("test") + } + + return build_system + + def _analyze_dependencies(self) -> Dict[str, Any]: + """ + Step 2: Analyze build dependencies and their impact + """ + logger.info("Analyzing dependencies...") + + if not self.package_json: + return {"analyzed": False, "message": "No package.json"} + + deps = self.package_json.get("dependencies", {}) + dev_deps = self.package_json.get("devDependencies", {}) + + analysis = { + "total_dependencies": len(deps), + "total_dev_dependencies": len(dev_deps), + "outdated": [], + "unused": [], + "large_dependencies": [], + "recommendations": [] + } + + # Check for common heavy dependencies + heavy_deps = ["moment", "lodash", "core-js"] + for dep in heavy_deps: + if dep in deps: + analysis["large_dependencies"].append({ + "name": dep, + "suggestion": f"Consider replacing {dep} with lighter alternative" + }) + + # Recommendations + if "moment" in deps: + analysis["recommendations"].append( + "Replace 'moment' with 'date-fns' or 'dayjs' for smaller bundle size" + ) + + if "lodash" in deps: + analysis["recommendations"].append( + "Use 'lodash-es' with tree-shaking or import specific lodash functions" + ) + + return analysis + + def _analyze_caching(self) -> Dict[str, Any]: + """ + Step 3: Analyze caching strategy + """ + logger.info("Analyzing caching strategy...") + + analysis = { + "cache_enabled": False, + "cache_type": "none", + "recommendations": [] + } + + # Check for cache directories + cache_dirs = [ + ".cache", + "node_modules/.cache", + ".webpack-cache", + ".vite" + ] + + for cache_dir in cache_dirs: + if (self.project_path / cache_dir).exists(): + analysis["cache_enabled"] = True + analysis["cache_type"] = cache_dir + break + + if not analysis["cache_enabled"]: + analysis["recommendations"].append( + "Enable persistent caching for faster incremental builds" + ) + + # Check for CI cache configuration + if (self.project_path / ".github" / "workflows").exists(): + analysis["ci_cache"] = "github-actions" + + return analysis + + def _analyze_bundling(self) -> Dict[str, Any]: + """ + Step 4: Analyze bundle configuration + """ + logger.info("Analyzing bundling configuration...") + + analysis = { + "code_splitting": "unknown", + "tree_shaking": "unknown", + "minification": "unknown", + "recommendations": [] + } + + # Check for build output + dist_dir = self.project_path / "dist" + build_dir = self.project_path / "build" + + output_dir = dist_dir if dist_dir.exists() else build_dir + + if output_dir and output_dir.exists(): + js_files = list(output_dir.glob("**/*.js")) + analysis["output_files"] = len(js_files) + + # Estimate if code splitting is used + if len(js_files) > 3: + analysis["code_splitting"] = "enabled" + elif len(js_files) <= 1: + analysis["code_splitting"] = "disabled" + analysis["recommendations"].append( + "Enable code splitting to reduce initial bundle size" + ) + + return analysis + + def _analyze_typescript(self) -> Dict[str, Any]: + """ + Step 5: Analyze TypeScript configuration + """ + logger.info("Analyzing TypeScript configuration...") + + tsconfig_path = self.project_path / "tsconfig.json" + + if not tsconfig_path.exists(): + return { + "enabled": False, + "message": "No TypeScript configuration found" + } + + with open(tsconfig_path, 'r') as f: + # Remove comments from JSON (basic approach) + content = f.read() + content = re.sub(r'//.*?\n', '\n', content) + content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) + tsconfig = json.loads(content) + + compiler_options = tsconfig.get("compilerOptions", {}) + + analysis = { + "enabled": True, + "incremental": compiler_options.get("incremental", False), + "skipLibCheck": compiler_options.get("skipLibCheck", False), + "composite": compiler_options.get("composite", False), + "recommendations": [] + } + + # Recommendations for faster compilation + if not analysis["incremental"]: + analysis["recommendations"].append( + "Enable 'incremental: true' in tsconfig.json for faster rebuilds" + ) + + if not analysis["skipLibCheck"]: + analysis["recommendations"].append( + "Enable 'skipLibCheck: true' to skip type checking of declaration files" + ) + + return analysis + + def _analyze_parallelization(self) -> Dict[str, Any]: + """ + Step 6: Analyze parallel processing opportunities + """ + logger.info("Analyzing parallelization opportunities...") + + analysis = { + "cpu_cores": self._get_cpu_count(), + "parallel_build": "unknown", + "recommendations": [] + } + + if self.build_system and self.build_system.get("type") == "webpack": + analysis["recommendations"].append( + "Consider using 'thread-loader' for parallel processing in Webpack" + ) + + if self.build_system and self.build_system.get("type") == "typescript": + analysis["recommendations"].append( + "Use 'ts-loader' with 'transpileOnly: true' or 'esbuild-loader' for faster TypeScript compilation" + ) + + return analysis + + def _generate_recommendations(self, analysis: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Generate prioritized recommendations based on analysis + """ + recommendations = [] + + # Collect all recommendations from analysis + for section, data in analysis.items(): + if isinstance(data, dict) and "recommendations" in data: + for rec in data["recommendations"]: + recommendations.append({ + "category": section, + "priority": "medium", + "description": rec + }) + + # Add high-priority recommendations + if analysis.get("caching", {}).get("cache_enabled") == False: + recommendations.insert(0, { + "category": "caching", + "priority": "high", + "description": "Enable persistent caching for significant build speed improvements" + }) + + if analysis.get("typescript", {}).get("incremental") == False: + recommendations.insert(0, { + "category": "typescript", + "priority": "high", + "description": "Enable incremental TypeScript compilation" + }) + + return recommendations + + def _estimate_improvement( + self, + analysis: Dict[str, Any], + recommendations: List[Dict[str, Any]] + ) -> str: + """ + Estimate potential build time improvement + """ + high_priority = sum(1 for r in recommendations if r.get("priority") == "high") + total = len(recommendations) + + if high_priority >= 3: + return "40-60% faster (multiple high-impact optimizations)" + elif high_priority >= 1: + return "20-40% faster (some high-impact optimizations)" + elif total >= 5: + return "10-20% faster (many small optimizations)" + elif total >= 1: + return "5-10% faster (few optimizations available)" + else: + return "Already well optimized" + + def _get_cpu_count(self) -> int: + """Get number of CPU cores""" + try: + import os + return os.cpu_count() or 1 + except: + return 1 + + def apply_optimizations(self, recommendations: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Apply recommended optimizations (interactive mode) + + Args: + recommendations: List of recommendations to apply + + Returns: + Results of optimization application + """ + results = { + "applied": [], + "skipped": [], + "failed": [] + } + + logger.info("Optimization application would happen here in full implementation") + logger.info("This is a demonstration skill showing the structure") + + return results + + +@telemetry_decorator(skill_name="build.optimize") +def main(): + """CLI entry point""" + parser = argparse.ArgumentParser( + description="Analyze and optimize build processes" + ) + parser.add_argument( + "project_path", + nargs="?", + default=".", + help="Path to project root (default: current directory)" + ) + parser.add_argument( + "--format", + choices=["json", "human"], + default="human", + help="Output format (default: human)" + ) + parser.add_argument( + "--apply", + action="store_true", + help="Apply recommended optimizations (interactive)" + ) + + args = parser.parse_args() + + try: + # Create optimizer + optimizer = BuildOptimizer(args.project_path) + + # Run analysis + results = optimizer.analyze() + + # Output results + if args.format == "json": + print(json.dumps(results, indent=2)) + else: + # Human-readable output + print(f"\n🔍 Build Optimization Analysis") + print(f"=" * 60) + print(f"Project: {results['project_path']}") + print(f"Build System: {results['build_system']['type']}") + print() + + # Dependencies + if "dependencies" in results["analysis"]: + dep = results["analysis"]["dependencies"] + print(f"📦 Dependencies:") + print(f" Total: {dep.get('total_dependencies', 0)}") + print(f" Dev: {dep.get('total_dev_dependencies', 0)}") + if dep.get("large_dependencies"): + print(f" Large deps: {len(dep['large_dependencies'])}") + print() + + # Caching + if "caching" in results["analysis"]: + cache = results["analysis"]["caching"] + print(f"💾 Caching:") + print(f" Enabled: {cache.get('cache_enabled', False)}") + print(f" Type: {cache.get('cache_type', 'none')}") + print() + + # TypeScript + if "typescript" in results["analysis"]: + ts = results["analysis"]["typescript"] + if ts.get("enabled"): + print(f"📘 TypeScript:") + print(f" Incremental: {ts.get('incremental', False)}") + print(f" Skip Lib Check: {ts.get('skipLibCheck', False)}") + print() + + # Recommendations + if results["recommendations"]: + print(f"💡 Recommendations ({len(results['recommendations'])}):") + print() + for i, rec in enumerate(results["recommendations"], 1): + priority_emoji = "🔴" if rec['priority'] == "high" else "🟡" + print(f" {i}. {priority_emoji} {rec['description']}") + print(f" Category: {rec['category']}") + print() + + print(f"⚡ Estimated Improvement: {results['estimated_improvement']}") + print() + + if args.apply: + print("Would you like to apply these optimizations?") + print("(Interactive application not yet implemented)") + + sys.exit(0) + + except BettyError as e: + print(format_error_response(str(e), "build.optimize")) + sys.exit(1) + except Exception as e: + logger.error(f"Unexpected error: {e}", exc_info=True) + print(format_error_response(f"Unexpected error: {e}", "build.optimize")) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/build.optimize/skill.yaml b/skills/build.optimize/skill.yaml new file mode 100644 index 0000000..e336536 --- /dev/null +++ b/skills/build.optimize/skill.yaml @@ -0,0 +1,121 @@ +name: build.optimize +version: 0.1.0 +description: | + Comprehensive build process optimization and analysis. + + Analyzes build systems (Webpack, Vite, Rollup, TypeScript, etc.) and provides + actionable recommendations for improving build speed and efficiency. + + Covers: + - Build system identification and analysis + - Dependency optimization + - Caching strategies + - Bundle analysis and code splitting + - TypeScript compilation optimization + - Parallelization opportunities + - Memory usage optimization + - CI/CD build improvements + +parameters: + - name: project_path + type: string + required: false + default: "." + description: "Path to project root directory" + + - name: format + type: enum + values: [json, human] + default: human + description: "Output format" + + - name: apply + type: boolean + default: false + description: "Apply recommended optimizations interactively" + +returns: + type: object + description: "Build optimization analysis results" + schema: + project_path: string + timestamp: string + build_system: + type: string + configs: array + analysis: + dependencies: object + caching: object + bundling: object + typescript: object + parallelization: object + recommendations: array + estimated_improvement: string + +execution: + type: script + entry_point: build_optimize.py + runtime: python3 + +dependencies: + - python: ">=3.8" + +tags: + - build + - optimization + - performance + - webpack + - vite + - typescript + +status: active + +examples: + - name: "Analyze current project" + command: "python3 skills/build.optimize/build_optimize.py" + description: "Analyze build configuration in current directory" + + - name: "Analyze specific project" + command: "python3 skills/build.optimize/build_optimize.py /path/to/project" + description: "Analyze build configuration in specified directory" + + - name: "JSON output" + command: "python3 skills/build.optimize/build_optimize.py --format=json" + description: "Output analysis results as JSON" + +documentation: + overview: | + The build.optimize skill provides comprehensive analysis of build processes + and generates prioritized recommendations for optimization. + + features: + - Automatic build system detection + - Dependency impact analysis + - Caching configuration review + - Bundle size and code splitting analysis + - TypeScript compilation optimization + - Parallel processing recommendations + - Estimated improvement calculations + + usage: | + Basic usage: + python3 skills/build.optimize/build_optimize.py + + For specific project: + python3 skills/build.optimize/build_optimize.py /path/to/project + + Machine-readable output: + python3 skills/build.optimize/build_optimize.py --format=json + + best_practices: + - Run analysis before making changes to establish baseline + - Address high-priority recommendations first + - Test build times before and after optimizations + - Keep cache directories in .gitignore + - Enable caching in CI/CD pipelines + + troubleshooting: + - If build system not detected, ensure package.json exists + - Some optimizations require manual configuration file edits + - TypeScript analysis requires valid tsconfig.json + - Results are most accurate with complete project structure diff --git a/skills/code.format/SKILL.md b/skills/code.format/SKILL.md new file mode 100644 index 0000000..16dfb5f --- /dev/null +++ b/skills/code.format/SKILL.md @@ -0,0 +1,278 @@ +# code.format + +Format code using Prettier, supporting multiple languages and file types. This skill can format individual files or entire directories, check formatting without making changes, and respect custom Prettier configurations. + +## Overview + +**Purpose:** Automatically format code using Prettier to maintain consistent code style across your project. + +**Command:** `/code/format` + +**Version:** 0.1.0 + +## Features + +- Format individual files or entire directories +- Support for 15+ file types (JavaScript, TypeScript, CSS, HTML, JSON, YAML, Markdown, and more) +- Auto-detect Prettier configuration files (.prettierrc, prettier.config.js, etc.) +- Check-only mode to validate formatting without modifying files +- Custom file pattern filtering +- Detailed formatting reports +- Automatic discovery of local and global Prettier installations + +## Supported File Types + +- **JavaScript**: .js, .jsx, .mjs, .cjs +- **TypeScript**: .ts, .tsx +- **CSS/Styles**: .css, .scss, .less +- **HTML**: .html, .htm +- **JSON**: .json +- **YAML**: .yaml, .yml +- **Markdown**: .md, .mdx +- **GraphQL**: .graphql, .gql +- **Vue**: .vue + +## Prerequisites + +Prettier must be installed either globally or locally in your project: + +```bash +# Global installation +npm install -g prettier + +# Or local installation (recommended) +npm install --save-dev prettier +``` + +## Usage + +### Basic Usage + +Format a single file: + +```bash +python3 skills/code.format/code_format.py --path src/index.js +``` + +Format an entire directory: + +```bash +python3 skills/code.format/code_format.py --path src/ +``` + +### Advanced Usage + +**Check formatting without modifying files:** + +```bash +python3 skills/code.format/code_format.py --path src/ --check +``` + +**Format only specific file types:** + +```bash +python3 skills/code.format/code_format.py --path src/ --patterns "**/*.ts,**/*.tsx" +``` + +**Use custom Prettier configuration:** + +```bash +python3 skills/code.format/code_format.py --path src/ --config-path .prettierrc.custom +``` + +**Dry run (check without writing):** + +```bash +python3 skills/code.format/code_format.py --path src/ --no-write +``` + +**Output as YAML:** + +```bash +python3 skills/code.format/code_format.py --path src/ --output-format yaml +``` + +## CLI Arguments + +| Argument | Required | Default | Description | +|----------|----------|---------|-------------| +| `--path` | Yes | - | File or directory path to format | +| `--config-path` | No | Auto-detect | Path to custom Prettier configuration file | +| `--check` | No | false | Only check formatting without modifying files | +| `--patterns` | No | All supported | Comma-separated glob patterns (e.g., "**/*.js,**/*.ts") | +| `--no-write` | No | false | Don't write changes (dry run mode) | +| `--output-format` | No | json | Output format: json or yaml | + +## Configuration + +The skill will automatically search for Prettier configuration files in this order: + +1. Custom config specified via `--config-path` +2. `.prettierrc` in the target directory or parent directories +3. `.prettierrc.json`, `.prettierrc.yml`, `.prettierrc.yaml` +4. `.prettierrc.js`, `.prettierrc.cjs` +5. `prettier.config.js`, `prettier.config.cjs` +6. Prettier defaults if no config found + +## Output Format + +The skill returns a JSON object with detailed formatting results: + +```json +{ + "ok": true, + "status": "success", + "message": "Formatted 5 files. 3 already formatted.", + "formatted_count": 5, + "already_formatted_count": 3, + "needs_formatting_count": 0, + "checked_count": 8, + "error_count": 0, + "files_formatted": [ + "src/components/Header.tsx", + "src/utils/helpers.js" + ], + "files_already_formatted": [ + "src/index.ts", + "src/App.tsx", + "src/config.json" + ], + "files_need_formatting": [], + "files_with_errors": [] +} +``` + +### Response Fields + +- **ok**: Boolean indicating overall success +- **status**: Status string ("success" or "failed") +- **message**: Human-readable summary +- **formatted_count**: Number of files that were formatted +- **already_formatted_count**: Number of files that were already properly formatted +- **needs_formatting_count**: Number of files that need formatting (check mode only) +- **checked_count**: Total number of files processed +- **error_count**: Number of files that encountered errors +- **files_formatted**: List of files that were formatted +- **files_already_formatted**: List of files that were already formatted +- **files_need_formatting**: List of files needing formatting (check mode) +- **files_with_errors**: List of files with errors and error messages + +## Error Handling + +The skill gracefully handles various error scenarios: + +- **Prettier not installed**: Clear error message with installation instructions +- **Invalid path**: Validation error if path doesn't exist +- **Syntax errors**: Reports files with syntax errors without stopping +- **Permission errors**: Reports files that couldn't be read/written +- **Timeouts**: 30-second timeout per file with clear error reporting + +## Integration with Agents + +Include this skill in your agent's configuration: + +```yaml +name: my.agent +version: 1.0.0 +skills_available: + - code.format +``` + +Then invoke it programmatically: + +```python +from skills.code_format.code_format import CodeFormat + +formatter = CodeFormat() +result = formatter.execute( + path="src/", + check_only=True, + file_patterns="**/*.{ts,tsx}" +) + +if result["ok"]: + print(f"Checked {result['checked_count']} files") + print(f"{result['needs_formatting_count']} files need formatting") +``` + +## Examples + +### Example 1: Format a React Project + +```bash +python3 skills/code.format/code_format.py \ + --path src/ \ + --patterns "**/*.{js,jsx,ts,tsx,css,json}" +``` + +### Example 2: Pre-commit Check + +```bash +python3 skills/code.format/code_format.py \ + --path src/ \ + --check \ + --output-format json + +# Exit code 0 if all files formatted, 1 otherwise +``` + +### Example 3: Format Only Changed Files + +```bash +# Get changed files from git +CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR | grep -E '\.(js|ts|jsx|tsx)$' | tr '\n' ',') + +# Format only those files +python3 skills/code.format/code_format.py \ + --path . \ + --patterns "$CHANGED_FILES" +``` + +## Testing + +Run the test suite: + +```bash +pytest skills/code.format/test_code_format.py -v +``` + +Run specific tests: + +```bash +pytest skills/code.format/test_code_format.py::TestCodeFormat::test_single_file -v +``` + +## Permissions + +This skill requires the following permissions: + +- **filesystem:read** - To read files and configurations +- **filesystem:write** - To write formatted files +- **process:execute** - To run the Prettier command + +## Artifact Metadata + +**Produces:** +- `formatting-report` (application/json) - Detailed formatting operation results + +## Troubleshooting + +**Issue**: "Prettier is not installed" +- **Solution**: Install Prettier globally (`npm install -g prettier`) or locally in your project + +**Issue**: No files found to format +- **Solution**: Check your file patterns and ensure files exist in the target path + +**Issue**: Configuration file not found +- **Solution**: Ensure your config file exists and the path is correct, or let it auto-detect + +**Issue**: Timeout errors +- **Solution**: Very large files may timeout (30s limit). Format them individually or increase timeout in code + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent, and enhanced with full Prettier integration. + +--- + +*Part of the Betty Framework* diff --git a/skills/code.format/__init__.py b/skills/code.format/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/code.format/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/code.format/code_format.py b/skills/code.format/code_format.py new file mode 100755 index 0000000..2155911 --- /dev/null +++ b/skills/code.format/code_format.py @@ -0,0 +1,434 @@ +#!/usr/bin/env python3 +""" +code.format - Format code using Prettier + +This skill formats code using Prettier, supporting multiple languages and file types. +It can format individual files or entire directories, check formatting without making +changes, and respect custom Prettier configurations. + +Generated by meta.skill with Betty Framework certification +""" + +import os +import sys +import json +import yaml +import subprocess +import shutil +from pathlib import Path +from typing import Dict, List, Any, Optional + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.certification import certified_skill + +logger = setup_logger(__name__) + + +class CodeFormat: + """ + Format code using Prettier, supporting multiple languages and file types. + """ + + # Supported file extensions + SUPPORTED_EXTENSIONS = { + '.js', '.jsx', '.mjs', '.cjs', # JavaScript + '.ts', '.tsx', # TypeScript + '.css', '.scss', '.less', # Styles + '.html', '.htm', # HTML + '.json', # JSON + '.yaml', '.yml', # YAML + '.md', '.mdx', # Markdown + '.graphql', '.gql', # GraphQL + '.vue', # Vue + } + + # Prettier config file names + CONFIG_FILES = [ + '.prettierrc', + '.prettierrc.json', + '.prettierrc.yml', + '.prettierrc.yaml', + '.prettierrc.json5', + '.prettierrc.js', + '.prettierrc.cjs', + 'prettier.config.js', + 'prettier.config.cjs', + ] + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + def _check_prettier_installed(self) -> tuple[bool, Optional[str]]: + """ + Check if Prettier is installed and return the command to use. + + Returns: + Tuple of (is_installed, command_path) + """ + # Check for npx prettier (local installation) + try: + result = subprocess.run( + ['npx', 'prettier', '--version'], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode == 0: + logger.info(f"Found Prettier via npx: {result.stdout.strip()}") + return True, 'npx prettier' + except (subprocess.SubprocessError, FileNotFoundError): + pass + + # Check for global prettier installation + prettier_path = shutil.which('prettier') + if prettier_path: + try: + result = subprocess.run( + ['prettier', '--version'], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode == 0: + logger.info(f"Found Prettier globally: {result.stdout.strip()}") + return True, 'prettier' + except subprocess.SubprocessError: + pass + + return False, None + + def _find_config_file(self, start_path: Path, custom_config: Optional[str] = None) -> Optional[Path]: + """ + Find Prettier configuration file. + + Args: + start_path: Path to start searching from + custom_config: Optional custom config file path + + Returns: + Path to config file or None + """ + if custom_config: + config_path = Path(custom_config) + if config_path.exists(): + logger.info(f"Using custom config: {config_path}") + return config_path + else: + logger.warning(f"Custom config not found: {config_path}") + + # Search upwards from start_path + current = start_path if start_path.is_dir() else start_path.parent + while current != current.parent: # Stop at root + for config_name in self.CONFIG_FILES: + config_path = current / config_name + if config_path.exists(): + logger.info(f"Found config: {config_path}") + return config_path + current = current.parent + + logger.info("No Prettier config found, will use defaults") + return None + + def _discover_files(self, path: Path, patterns: Optional[List[str]] = None) -> List[Path]: + """ + Discover files to format. + + Args: + path: File or directory path + patterns: Optional glob patterns to filter files + + Returns: + List of file paths to format + """ + if path.is_file(): + return [path] + + files = [] + if patterns: + for pattern in patterns: + files.extend(path.rglob(pattern)) + else: + # Find all files with supported extensions + for ext in self.SUPPORTED_EXTENSIONS: + files.extend(path.rglob(f'*{ext}')) + + # Filter out common ignore patterns + ignored_dirs = {'node_modules', '.git', 'dist', 'build', '.next', 'coverage', '__pycache__'} + filtered_files = [ + f for f in files + if f.is_file() and not any(ignored in f.parts for ignored in ignored_dirs) + ] + + logger.info(f"Discovered {len(filtered_files)} files to format") + return filtered_files + + def _format_file(self, file_path: Path, prettier_cmd: str, check_only: bool = False, + config_path: Optional[Path] = None) -> Dict[str, Any]: + """ + Format a single file using Prettier. + + Args: + file_path: Path to file to format + prettier_cmd: Prettier command to use + check_only: Only check formatting without modifying + config_path: Optional path to config file + + Returns: + Dict with formatting result + """ + cmd = prettier_cmd.split() + cmd.append(str(file_path)) + + if check_only: + cmd.append('--check') + else: + cmd.append('--write') + + if config_path: + cmd.extend(['--config', str(config_path)]) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode == 0: + if check_only and result.stdout: + # File is already formatted + return { + 'file': str(file_path), + 'status': 'already_formatted', + 'ok': True + } + else: + # File was formatted + return { + 'file': str(file_path), + 'status': 'formatted', + 'ok': True + } + else: + # Formatting failed or file needs formatting (in check mode) + if check_only and 'Code style issues' in result.stderr: + return { + 'file': str(file_path), + 'status': 'needs_formatting', + 'ok': True + } + else: + return { + 'file': str(file_path), + 'status': 'error', + 'ok': False, + 'error': result.stderr or result.stdout + } + + except subprocess.TimeoutExpired: + logger.error(f"Timeout formatting {file_path}") + return { + 'file': str(file_path), + 'status': 'error', + 'ok': False, + 'error': 'Timeout after 30 seconds' + } + except Exception as e: + logger.error(f"Error formatting {file_path}: {e}") + return { + 'file': str(file_path), + 'status': 'error', + 'ok': False, + 'error': str(e) + } + + @certified_skill("code.format") + def execute( + self, + path: str, + config_path: Optional[str] = None, + check_only: bool = False, + file_patterns: Optional[str] = None, + write: bool = True + ) -> Dict[str, Any]: + """ + Execute the code formatting skill. + + Args: + path: File or directory path to format + config_path: Path to custom Prettier configuration file + check_only: Only check formatting without modifying files + file_patterns: Comma-separated glob patterns to filter files + write: Write formatted output to files (default: True) + + Returns: + Dict with execution results including: + - ok: Overall success status + - status: Status message + - formatted_count: Number of files formatted + - checked_count: Number of files checked + - error_count: Number of files with errors + - files_formatted: List of formatted file paths + - files_already_formatted: List of already formatted file paths + - files_with_errors: List of files that had errors + """ + try: + logger.info(f"Executing code.format on: {path}") + + # Check if Prettier is installed + is_installed, prettier_cmd = self._check_prettier_installed() + if not is_installed: + return { + "ok": False, + "status": "failed", + "error": "Prettier is not installed. Install it with: npm install -g prettier or npm install --save-dev prettier" + } + + # Validate path + target_path = Path(path) + if not target_path.exists(): + return { + "ok": False, + "status": "failed", + "error": f"Path does not exist: {path}" + } + + # Find config file + config_file = self._find_config_file(target_path, config_path) + + # Parse file patterns + patterns = None + if file_patterns: + patterns = [p.strip() for p in file_patterns.split(',')] + + # Discover files + files = self._discover_files(target_path, patterns) + if not files: + return { + "ok": True, + "status": "success", + "message": "No files found to format", + "formatted_count": 0, + "checked_count": 0, + "error_count": 0 + } + + # Format files + results = [] + for file_path in files: + result = self._format_file( + file_path, + prettier_cmd, + check_only=check_only or not write, + config_path=config_file + ) + results.append(result) + + # Aggregate results + files_formatted = [r['file'] for r in results if r['status'] == 'formatted'] + files_already_formatted = [r['file'] for r in results if r['status'] == 'already_formatted'] + files_need_formatting = [r['file'] for r in results if r['status'] == 'needs_formatting'] + files_with_errors = [ + {'file': r['file'], 'error': r.get('error', 'Unknown error')} + for r in results if r['status'] == 'error' + ] + + response = { + "ok": True, + "status": "success", + "formatted_count": len(files_formatted), + "already_formatted_count": len(files_already_formatted), + "needs_formatting_count": len(files_need_formatting), + "checked_count": len(files), + "error_count": len(files_with_errors), + "files_formatted": files_formatted, + "files_already_formatted": files_already_formatted, + "files_need_formatting": files_need_formatting, + "files_with_errors": files_with_errors + } + + if check_only: + response["message"] = f"Checked {len(files)} files. {len(files_need_formatting)} need formatting." + else: + response["message"] = f"Formatted {len(files_formatted)} files. {len(files_already_formatted)} already formatted." + + logger.info(f"Skill completed: {response['message']}") + return response + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Format code using Prettier, supporting multiple languages and file types." + ) + + parser.add_argument( + "--path", + required=True, + help="File or directory path to format" + ) + parser.add_argument( + "--config-path", + help="Path to custom Prettier configuration file" + ) + parser.add_argument( + "--check", + action="store_true", + help="Only check formatting without modifying files" + ) + parser.add_argument( + "--patterns", + help="Comma-separated glob patterns to filter files (e.g., '**/*.js,**/*.ts')" + ) + parser.add_argument( + "--no-write", + action="store_true", + help="Don't write changes (dry run)" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = CodeFormat() + + # Execute skill + result = skill.execute( + path=args.path, + config_path=args.config_path, + check_only=args.check, + file_patterns=args.patterns, + write=not args.no_write + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/code.format/skill.yaml b/skills/code.format/skill.yaml new file mode 100644 index 0000000..18c5f88 --- /dev/null +++ b/skills/code.format/skill.yaml @@ -0,0 +1,56 @@ +name: code.format +version: 0.1.0 +description: Format code using Prettier, supporting multiple languages and file types. + This skill can format individual files or entire directories, check formatting without + making changes, and respect custom Prettier configurations. + +inputs: + - name: path + type: string + required: true + description: File or directory path to format + - name: config_path + type: string + required: false + description: Path to custom Prettier configuration file + - name: check_only + type: boolean + required: false + default: false + description: Only check formatting without modifying files + - name: file_patterns + type: string + required: false + description: Comma-separated glob patterns to filter files (e.g., "**/*.js,**/*.ts") + - name: write + type: boolean + required: false + default: true + description: Write formatted output to files (default true, use false for dry run) + +outputs: + - name: formatting_report.json + type: application/json + description: JSON report with formatting results, files processed, and any errors + - name: formatted_files + type: text/plain + description: Updated files with proper formatting (when write=true) + +status: active + +permissions: + - filesystem:read + - filesystem:write + - process:execute + +entrypoints: + - command: /code/format + handler: code_format.py + runtime: python + description: Format code using Prettier + +artifact_metadata: + produces: + - type: formatting-report + format: application/json + description: Detailed report of formatting operation results diff --git a/skills/code.format/test_code_format.py b/skills/code.format/test_code_format.py new file mode 100644 index 0000000..feb4ef2 --- /dev/null +++ b/skills/code.format/test_code_format.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 +""" +Tests for code.format skill + +Generated by meta.skill and enhanced with comprehensive test coverage +""" + +import pytest +import sys +import os +import tempfile +import shutil +from pathlib import Path +from unittest.mock import Mock, patch, MagicMock + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.code_format import code_format + + +class TestCodeFormat: + """Tests for CodeFormat skill""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = code_format.CodeFormat() + self.temp_dir = tempfile.mkdtemp() + + def teardown_method(self): + """Cleanup test fixtures""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + assert isinstance(self.skill.SUPPORTED_EXTENSIONS, set) + assert len(self.skill.SUPPORTED_EXTENSIONS) > 0 + assert isinstance(self.skill.CONFIG_FILES, list) + assert len(self.skill.CONFIG_FILES) > 0 + + def test_supported_extensions(self): + """Test that all expected file types are supported""" + extensions = self.skill.SUPPORTED_EXTENSIONS + + # Check JavaScript extensions + assert '.js' in extensions + assert '.jsx' in extensions + assert '.mjs' in extensions + assert '.cjs' in extensions + + # Check TypeScript extensions + assert '.ts' in extensions + assert '.tsx' in extensions + + # Check style extensions + assert '.css' in extensions + assert '.scss' in extensions + assert '.less' in extensions + + # Check other formats + assert '.json' in extensions + assert '.yaml' in extensions + assert '.md' in extensions + assert '.html' in extensions + + def test_check_prettier_installed_with_npx(self): + """Test Prettier detection via npx""" + with patch('subprocess.run') as mock_run: + mock_run.return_value = Mock(returncode=0, stdout="3.0.0\n") + + is_installed, cmd = self.skill._check_prettier_installed() + + assert is_installed is True + assert cmd == 'npx prettier' + + def test_check_prettier_installed_global(self): + """Test Prettier detection via global installation""" + with patch('subprocess.run') as mock_run: + # First call (npx) fails + mock_run.side_effect = [ + FileNotFoundError(), + Mock(returncode=0, stdout="3.0.0\n") + ] + + with patch('shutil.which', return_value='/usr/local/bin/prettier'): + is_installed, cmd = self.skill._check_prettier_installed() + + assert is_installed is True + assert cmd == 'prettier' + + def test_check_prettier_not_installed(self): + """Test when Prettier is not installed""" + with patch('subprocess.run', side_effect=FileNotFoundError()): + with patch('shutil.which', return_value=None): + is_installed, cmd = self.skill._check_prettier_installed() + + assert is_installed is False + assert cmd is None + + def test_find_config_file_custom(self): + """Test finding custom config file""" + # Create a custom config file + config_path = Path(self.temp_dir) / '.prettierrc.custom' + config_path.write_text('{"semi": true}') + + found_config = self.skill._find_config_file( + Path(self.temp_dir), + custom_config=str(config_path) + ) + + assert found_config == config_path + + def test_find_config_file_auto_detect(self): + """Test auto-detecting config file""" + # Create a .prettierrc file + config_path = Path(self.temp_dir) / '.prettierrc' + config_path.write_text('{"semi": true}') + + found_config = self.skill._find_config_file(Path(self.temp_dir)) + + assert found_config == config_path + + def test_find_config_file_none(self): + """Test when no config file exists""" + found_config = self.skill._find_config_file(Path(self.temp_dir)) + assert found_config is None + + def test_discover_files_single_file(self): + """Test discovering a single file""" + test_file = Path(self.temp_dir) / 'test.js' + test_file.write_text('console.log("test");') + + files = self.skill._discover_files(test_file) + + assert len(files) == 1 + assert files[0] == test_file + + def test_discover_files_directory(self): + """Test discovering files in a directory""" + # Create test files + (Path(self.temp_dir) / 'test1.js').write_text('console.log("test1");') + (Path(self.temp_dir) / 'test2.ts').write_text('console.log("test2");') + (Path(self.temp_dir) / 'test.txt').write_text('not supported') + + files = self.skill._discover_files(Path(self.temp_dir)) + + assert len(files) == 2 + assert any(f.name == 'test1.js' for f in files) + assert any(f.name == 'test2.ts' for f in files) + + def test_discover_files_with_patterns(self): + """Test discovering files with glob patterns""" + # Create test files + (Path(self.temp_dir) / 'test1.js').write_text('console.log("test1");') + (Path(self.temp_dir) / 'test2.ts').write_text('console.log("test2");') + (Path(self.temp_dir) / 'test3.css').write_text('body { margin: 0; }') + + files = self.skill._discover_files(Path(self.temp_dir), patterns=['*.js']) + + assert len(files) == 1 + assert files[0].name == 'test1.js' + + def test_discover_files_ignores_node_modules(self): + """Test that node_modules is ignored""" + # Create node_modules directory with files + node_modules = Path(self.temp_dir) / 'node_modules' + node_modules.mkdir() + (node_modules / 'test.js').write_text('console.log("test");') + + # Create regular file + (Path(self.temp_dir) / 'app.js').write_text('console.log("app");') + + files = self.skill._discover_files(Path(self.temp_dir)) + + assert len(files) == 1 + assert files[0].name == 'app.js' + + def test_format_file_success(self): + """Test formatting a file successfully""" + test_file = Path(self.temp_dir) / 'test.js' + test_file.write_text('console.log("test");') + + with patch('subprocess.run') as mock_run: + mock_run.return_value = Mock(returncode=0, stdout='', stderr='') + + result = self.skill._format_file(test_file, 'prettier', check_only=False) + + assert result['ok'] is True + assert result['status'] == 'formatted' + assert result['file'] == str(test_file) + + def test_format_file_check_mode_needs_formatting(self): + """Test check mode when file needs formatting""" + test_file = Path(self.temp_dir) / 'test.js' + test_file.write_text('console.log("test");') + + with patch('subprocess.run') as mock_run: + mock_run.return_value = Mock( + returncode=1, + stdout='', + stderr='Code style issues found' + ) + + result = self.skill._format_file(test_file, 'prettier', check_only=True) + + assert result['ok'] is True + assert result['status'] == 'needs_formatting' + + def test_format_file_error(self): + """Test formatting with error""" + test_file = Path(self.temp_dir) / 'test.js' + test_file.write_text('invalid syntax {{{') + + with patch('subprocess.run') as mock_run: + mock_run.return_value = Mock( + returncode=1, + stdout='', + stderr='Syntax error' + ) + + result = self.skill._format_file(test_file, 'prettier', check_only=False) + + assert result['ok'] is False + assert result['status'] == 'error' + assert 'error' in result + + def test_execute_prettier_not_installed(self): + """Test execute when Prettier is not installed""" + with patch.object(self.skill, '_check_prettier_installed', return_value=(False, None)): + result = self.skill.execute(path=self.temp_dir) + + assert result['ok'] is False + assert result['status'] == 'failed' + assert 'not installed' in result['error'].lower() + + def test_execute_invalid_path(self): + """Test execute with invalid path""" + with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')): + result = self.skill.execute(path='/nonexistent/path') + + assert result['ok'] is False + assert result['status'] == 'failed' + assert 'does not exist' in result['error'].lower() + + def test_execute_no_files(self): + """Test execute when no files are found""" + with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')): + with patch.object(self.skill, '_discover_files', return_value=[]): + result = self.skill.execute(path=self.temp_dir) + + assert result['ok'] is True + assert result['status'] == 'success' + assert result['formatted_count'] == 0 + assert 'No files found' in result['message'] + + def test_execute_successful_formatting(self): + """Test successful formatting execution""" + # Create test files + test_file = Path(self.temp_dir) / 'test.js' + test_file.write_text('console.log("test");') + + with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')): + with patch.object(self.skill, '_format_file') as mock_format: + mock_format.return_value = { + 'ok': True, + 'status': 'formatted', + 'file': str(test_file) + } + + result = self.skill.execute(path=str(test_file)) + + assert result['ok'] is True + assert result['status'] == 'success' + assert result['formatted_count'] == 1 + assert result['error_count'] == 0 + + def test_execute_check_mode(self): + """Test execute in check mode""" + test_file = Path(self.temp_dir) / 'test.js' + test_file.write_text('console.log("test");') + + with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')): + with patch.object(self.skill, '_format_file') as mock_format: + mock_format.return_value = { + 'ok': True, + 'status': 'needs_formatting', + 'file': str(test_file) + } + + result = self.skill.execute(path=str(test_file), check_only=True) + + assert result['ok'] is True + assert result['status'] == 'success' + assert result['needs_formatting_count'] == 1 + assert 'need formatting' in result['message'].lower() + + def test_execute_with_patterns(self): + """Test execute with file patterns""" + # Create test files + (Path(self.temp_dir) / 'test.js').write_text('console.log("test");') + (Path(self.temp_dir) / 'test.ts').write_text('console.log("test");') + + with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')): + with patch.object(self.skill, '_discover_files') as mock_discover: + mock_discover.return_value = [Path(self.temp_dir) / 'test.js'] + + result = self.skill.execute( + path=self.temp_dir, + file_patterns='*.js' + ) + + # Verify patterns were parsed correctly + mock_discover.assert_called_once() + call_args = mock_discover.call_args + assert call_args[0][1] == ['*.js'] + + def test_execute_with_errors(self): + """Test execute when some files have errors""" + test_file = Path(self.temp_dir) / 'test.js' + test_file.write_text('invalid') + + with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')): + with patch.object(self.skill, '_format_file') as mock_format: + mock_format.return_value = { + 'ok': False, + 'status': 'error', + 'file': str(test_file), + 'error': 'Syntax error' + } + + result = self.skill.execute(path=str(test_file)) + + assert result['ok'] is True # Overall success even with file errors + assert result['status'] == 'success' + assert result['error_count'] == 1 + assert len(result['files_with_errors']) == 1 + + def test_execute_exception_handling(self): + """Test execute handles exceptions gracefully""" + with patch.object(self.skill, '_check_prettier_installed', side_effect=Exception('Test error')): + result = self.skill.execute(path=self.temp_dir) + + assert result['ok'] is False + assert result['status'] == 'failed' + assert 'error' in result + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["code_format.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + code_format.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Format code using Prettier" in captured.out + + +def test_cli_missing_path(capsys): + """Test CLI with missing required path argument""" + sys.argv = ["code_format.py"] + + with pytest.raises(SystemExit) as exc_info: + code_format.main() + + assert exc_info.value.code != 0 + + +def test_cli_execution(): + """Test CLI execution with mocked skill""" + sys.argv = ["code_format.py", "--path", "/tmp", "--check"] + + with patch.object(code_format.CodeFormat, 'execute') as mock_execute: + mock_execute.return_value = { + 'ok': True, + 'status': 'success', + 'message': 'Test' + } + + with pytest.raises(SystemExit) as exc_info: + code_format.main() + + assert exc_info.value.code == 0 + mock_execute.assert_called_once() + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/command.define/SKILL.md b/skills/command.define/SKILL.md new file mode 100644 index 0000000..8bb3068 --- /dev/null +++ b/skills/command.define/SKILL.md @@ -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 +``` + +### 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 diff --git a/skills/command.define/__init__.py b/skills/command.define/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/command.define/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/command.define/command_define.py b/skills/command.define/command_define.py new file mode 100755 index 0000000..83e7c10 --- /dev/null +++ b/skills/command.define/command_define.py @@ -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 " + 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() diff --git a/skills/command.define/skill.yaml b/skills/command.define/skill.yaml new file mode 100644 index 0000000..53267df --- /dev/null +++ b/skills/command.define/skill.yaml @@ -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] diff --git a/skills/config.generate.router/generate_router.py b/skills/config.generate.router/generate_router.py new file mode 100755 index 0000000..8d134fb --- /dev/null +++ b/skills/config.generate.router/generate_router.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +""" +Skill: config.generate.router +Generates Claude Code Router configuration +""" + +import json +import sys +from datetime import datetime +from typing import Dict, List, Any + + +class RouterConfigGenerator: + """Generates router configuration for Claude Code""" + + CONFIG_VERSION = "1.0.0" + + def generate( + self, + llm_backends: List[Dict[str, Any]], + routing_rules: Dict[str, Any], + config_options: Dict[str, Any] = None + ) -> Dict[str, Any]: + """ + Generate router configuration matching Claude Code Router schema + + Args: + llm_backends: List of backend provider configs + routing_rules: Dictionary of routing context mappings + config_options: Optional config settings (LOG, API_TIMEOUT_MS, etc.) + + Returns: + Complete router configuration in Claude Code Router format + """ + options = config_options or {} + + config = { + "Providers": self._format_providers(llm_backends), + "Router": self._format_router(routing_rules) + } + + # Add optional configuration fields if provided + if "LOG" in options: + config["LOG"] = options["LOG"] + if "LOG_LEVEL" in options: + config["LOG_LEVEL"] = options["LOG_LEVEL"] + if "API_TIMEOUT_MS" in options: + config["API_TIMEOUT_MS"] = options["API_TIMEOUT_MS"] + if "NON_INTERACTIVE_MODE" in options: + config["NON_INTERACTIVE_MODE"] = options["NON_INTERACTIVE_MODE"] + if "APIKEY" in options: + config["APIKEY"] = options["APIKEY"] + if "PROXY_URL" in options: + config["PROXY_URL"] = options["PROXY_URL"] + if "CUSTOM_ROUTER_PATH" in options: + config["CUSTOM_ROUTER_PATH"] = options["CUSTOM_ROUTER_PATH"] + if "HOST" in options: + config["HOST"] = options["HOST"] + + return config + + def _format_providers(self, backends: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Format provider configurations for Claude Code Router""" + formatted = [] + for backend in backends: + entry = { + "name": backend["name"], + "api_base_url": backend["api_base_url"], + "models": backend["models"] + } + + # Only include API key if present (not for local providers) + if backend.get("api_key"): + entry["api_key"] = backend["api_key"] + + # Include transformer if specified + if backend.get("transformer"): + entry["transformer"] = backend["transformer"] + + # Include any additional provider-specific settings + for key, value in backend.items(): + if key not in ["name", "api_base_url", "models", "api_key", "transformer"]: + entry[key] = value + + formatted.append(entry) + + return formatted + + def _format_router(self, routing_rules: Dict[str, Any]) -> Dict[str, str]: + """ + Format routing rules for Claude Code Router + + Converts from object format to "provider,model" string format: + Input: {"provider": "openrouter", "model": "claude-3.5-sonnet"} + Output: "openrouter,claude-3.5-sonnet" + """ + formatted = {} + for context, rule in routing_rules.items(): + provider = rule["provider"] + model = rule["model"] + # Claude Code Router expects "provider,model" string format + formatted[context] = f"{provider},{model}" + + return formatted + + +def main(): + """CLI entrypoint""" + if len(sys.argv) < 2: + print(json.dumps({ + "error": "Usage: generate_router.py " + })) + sys.exit(1) + + try: + input_data = json.loads(sys.argv[1]) + + generator = RouterConfigGenerator() + config = generator.generate( + llm_backends=input_data.get("llm_backends", []), + routing_rules=input_data.get("routing_rules", {}), + config_options=input_data.get("config_options", {}) + ) + + print(json.dumps(config, indent=2)) + sys.exit(0) + + except json.JSONDecodeError as e: + print(json.dumps({ + "error": f"Invalid JSON: {e}" + })) + sys.exit(1) + except Exception as e: + print(json.dumps({ + "error": f"Generation error: {e}" + })) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/config.generate.router/skill.yaml b/skills/config.generate.router/skill.yaml new file mode 100644 index 0000000..393f889 --- /dev/null +++ b/skills/config.generate.router/skill.yaml @@ -0,0 +1,66 @@ +name: config.generate.router +version: 0.1.0 +description: Generates valid Claude Code Router configuration JSON from validated inputs +status: active + +inputs: + - name: llm_backends + type: array + required: true + description: List of validated backend provider configurations + + - name: routing_rules + type: object + required: true + description: Validated routing context mappings + + - name: metadata + type: object + required: false + description: Optional metadata (audit_id, environment, etc.) + +outputs: + - name: router_config + type: object + description: Complete router configuration ready for file output + schema: + type: object + properties: + version: + type: string + generated_at: + type: string + backends: + type: array + routing: + type: object + metadata: + type: object + +artifact_metadata: + consumes: + - type: validation-report + description: Validation report confirming input correctness + content_type: application/json + schema: schemas/validation-report.json + + produces: + - type: llm-router-config + description: Complete Claude Code Router configuration + file_pattern: "config.json" + content_type: application/json + schema: schemas/router-config.json + +entrypoints: + - command: /skill/config/generate/router + handler: generate_router.py + runtime: python + +permissions: + - filesystem:read + +tags: + - config + - generation + - router + - llm diff --git a/skills/config.validate.router/skill.yaml b/skills/config.validate.router/skill.yaml new file mode 100644 index 0000000..4095f4f --- /dev/null +++ b/skills/config.validate.router/skill.yaml @@ -0,0 +1,96 @@ +name: config.validate.router +version: 0.1.0 +description: Validates Claude Code Router configuration inputs for correctness, completeness, and schema compliance +status: active + +inputs: + - name: llm_backends + type: array + required: true + description: List of backend provider configurations + schema: + type: array + items: + type: object + properties: + name: + type: string + description: Provider name (e.g., openrouter, ollama, claude) + api_base_url: + type: string + description: Base URL for the provider API + api_key: + type: string + description: API key (optional for local providers) + models: + type: array + items: + type: string + description: List of model identifiers + required: + - name + - api_base_url + - models + + - name: routing_rules + type: object + required: true + description: Dictionary mapping Claude routing contexts to provider/model pairs + schema: + type: object + properties: + default: + type: object + think: + type: object + background: + type: object + longContext: + type: object + additionalProperties: true + +outputs: + - name: validation_result + type: object + description: Validation result with status and errors + schema: + type: object + properties: + valid: + type: boolean + errors: + type: array + items: + type: string + warnings: + type: array + items: + type: string + +artifact_metadata: + consumes: + - type: router-config-input + description: Raw router configuration input before validation + content_type: application/json + schema: schemas/router-config-input.json + + produces: + - type: validation-report + description: Validation report with errors and warnings + file_pattern: "*-validation-report.json" + content_type: application/json + schema: schemas/validation-report.json + +entrypoints: + - command: /skill/config/validate/router + handler: validate_router.py + runtime: python + +permissions: + - filesystem:read + +tags: + - validation + - config + - router + - llm diff --git a/skills/config.validate.router/validate_router.py b/skills/config.validate.router/validate_router.py new file mode 100755 index 0000000..ed78233 --- /dev/null +++ b/skills/config.validate.router/validate_router.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +Skill: config.validate.router +Validates Claude Code Router configuration inputs +""" + +import json +import sys +from typing import Dict, List, Any + + +class RouterConfigValidator: + """Validates router configuration for Claude Code Router""" + + # Claude Code Router supports these routing contexts + VALID_ROUTING_CONTEXTS = {"default", "think", "background", "longContext", "webSearch", "image"} + REQUIRED_PROVIDER_FIELDS = {"name", "api_base_url", "models"} + REQUIRED_ROUTING_FIELDS = {"provider", "model"} + + def __init__(self): + self.errors: List[str] = [] + self.warnings: List[str] = [] + + def validate(self, llm_backends: List[Dict[str, Any]], routing_rules: Dict[str, Any]) -> Dict[str, Any]: + """ + Validate router configuration + + Args: + llm_backends: List of backend provider configs + routing_rules: Dictionary of routing context mappings + + Returns: + Validation result with status, errors, and warnings + """ + self.errors = [] + self.warnings = [] + + # Validate backends + self._validate_backends(llm_backends) + + # Validate routing rules + self._validate_routing_rules(routing_rules, llm_backends) + + return { + "valid": len(self.errors) == 0, + "errors": self.errors, + "warnings": self.warnings + } + + def _validate_backends(self, backends: List[Dict[str, Any]]) -> None: + """Validate backend provider configurations""" + if not backends: + self.errors.append("llm_backends cannot be empty") + return + + seen_names = set() + for idx, backend in enumerate(backends): + # Check required fields + missing = self.REQUIRED_PROVIDER_FIELDS - set(backend.keys()) + if missing: + self.errors.append(f"Backend {idx}: missing required fields {missing}") + + # Check name uniqueness + name = backend.get("name") + if name: + if name in seen_names: + self.errors.append(f"Duplicate backend name: {name}") + seen_names.add(name) + + # Validate models list + models = backend.get("models", []) + if not isinstance(models, list): + self.errors.append(f"Backend {name or idx}: 'models' must be a list") + elif not models: + self.errors.append(f"Backend {name or idx}: 'models' cannot be empty") + + # Validate API base URL format + api_base_url = backend.get("api_base_url", "") + if api_base_url and not (api_base_url.startswith("http://") or + api_base_url.startswith("https://")): + self.warnings.append( + f"Backend {name or idx}: api_base_url should start with http:// or https://" + ) + + # Check for API key in local providers + if "localhost" in api_base_url or "127.0.0.1" in api_base_url: + if backend.get("api_key"): + self.warnings.append( + f"Backend {name or idx}: Local provider has api_key (may be unnecessary)" + ) + elif not backend.get("api_key"): + self.warnings.append( + f"Backend {name or idx}: Remote provider missing api_key" + ) + + def _validate_routing_rules( + self, + routing_rules: Dict[str, Any], + backends: List[Dict[str, Any]] + ) -> None: + """Validate routing rule mappings""" + if not routing_rules: + self.errors.append("routing_rules cannot be empty") + return + + # Build provider-model map + provider_models = {} + for backend in backends: + name = backend.get("name") + models = backend.get("models", []) + if name: + provider_models[name] = set(models) + + # Validate each routing context + for context, rule in routing_rules.items(): + # Warn about unknown contexts + if context not in self.VALID_ROUTING_CONTEXTS: + self.warnings.append(f"Unknown routing context: {context}") + + # Check required fields + if not isinstance(rule, dict): + self.errors.append(f"Routing rule '{context}' must be an object") + continue + + missing = self.REQUIRED_ROUTING_FIELDS - set(rule.keys()) + if missing: + self.errors.append( + f"Routing rule '{context}': missing required fields {missing}" + ) + continue + + provider = rule.get("provider") + model = rule.get("model") + + # Validate provider exists + if provider not in provider_models: + self.errors.append( + f"Routing rule '{context}': unknown provider '{provider}'" + ) + continue + + # Validate model exists for provider + if model not in provider_models[provider]: + self.errors.append( + f"Routing rule '{context}': model '{model}' not available " + f"in provider '{provider}'" + ) + + # Check for missing essential contexts + essential = {"default"} + missing_essential = essential - set(routing_rules.keys()) + if missing_essential: + self.errors.append(f"Missing essential routing contexts: {missing_essential}") + + +def main(): + """CLI entrypoint""" + if len(sys.argv) < 2: + print(json.dumps({ + "valid": False, + "errors": ["Usage: validate_router.py "], + "warnings": [] + })) + sys.exit(1) + + try: + config = json.loads(sys.argv[1]) + + validator = RouterConfigValidator() + result = validator.validate( + llm_backends=config.get("llm_backends", []), + routing_rules=config.get("routing_rules", {}) + ) + + print(json.dumps(result, indent=2)) + sys.exit(0 if result["valid"] else 1) + + except json.JSONDecodeError as e: + print(json.dumps({ + "valid": False, + "errors": [f"Invalid JSON: {e}"], + "warnings": [] + })) + sys.exit(1) + except Exception as e: + print(json.dumps({ + "valid": False, + "errors": [f"Validation error: {e}"], + "warnings": [] + })) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/data.transform/README.md b/skills/data.transform/README.md new file mode 100644 index 0000000..407f8a3 --- /dev/null +++ b/skills/data.transform/README.md @@ -0,0 +1,82 @@ +# data.transform + +Transform data between different formats (JSON, YAML, XML, CSV) with validation and error handling + +## Overview + +**Purpose:** Transform data between different formats (JSON, YAML, XML, CSV) with validation and error handling + +**Command:** `/data/transform` + +## Usage + +### Basic Usage + +```bash +python3 skills/data/transform/data_transform.py +``` + +### With Arguments + +```bash +python3 skills/data/transform/data_transform.py \ + --input_file_path "value" \ + --source_format "value" \ + --target_format "value" \ + --schema_path_(optional) "value" \ + --output-format json +``` + +## Inputs + +- **input_file_path** +- **source_format** +- **target_format** +- **schema_path (optional)** + +## Outputs + +- **transformed_file** +- **transformation_report.json** + +## Artifact Metadata + +### Produces + +- `transformed-data` +- `transformation-report` + +## Permissions + +- `filesystem:read` +- `filesystem:write` + +## Implementation Notes + +Support transformations between: - JSON ↔ YAML - JSON ↔ XML - JSON ↔ CSV - YAML ↔ XML - XML ↔ CSV Features: - Validate input against schema before transformation - Preserve data types during conversion - Handle nested structures appropriately - Report data loss warnings (e.g., CSV can't represent nesting) - Support custom transformation rules - Provide detailed error messages Output report should include: - Transformation success status - Source and target formats - Data validation results - Warnings about potential data loss - Transformation time and file sizes + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - data.transform +``` + +## Testing + +Run tests with: + +```bash +pytest skills/data/transform/test_data_transform.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/data.transform/__init__.py b/skills/data.transform/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/data.transform/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/data.transform/data_transform.py b/skills/data.transform/data_transform.py new file mode 100755 index 0000000..524688b --- /dev/null +++ b/skills/data.transform/data_transform.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +data.transform - Transform data between different formats (JSON, YAML, XML, CSV) with validation and error handling + +Generated by meta.skill +""" + +import os +import sys +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +class DataTransform: + """ + Transform data between different formats (JSON, YAML, XML, CSV) with validation and error handling + """ + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + def execute(self, input_file_path: Optional[str] = None, source_format: Optional[str] = None, target_format: Optional[str] = None, schema_path_optional: Optional[str] = None) -> Dict[str, Any]: + """ + Execute the skill + + Returns: + Dict with execution results + """ + try: + logger.info("Executing data.transform...") + + # TODO: Implement skill logic here + + # Implementation notes: + # Support transformations between: - JSON ↔ YAML - JSON ↔ XML - JSON ↔ CSV - YAML ↔ XML - XML ↔ CSV Features: - Validate input against schema before transformation - Preserve data types during conversion - Handle nested structures appropriately - Report data loss warnings (e.g., CSV can't represent nesting) - Support custom transformation rules - Provide detailed error messages Output report should include: - Transformation success status - Source and target formats - Data validation results - Warnings about potential data loss - Transformation time and file sizes + + # Placeholder implementation + result = { + "ok": True, + "status": "success", + "message": "Skill executed successfully" + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Transform data between different formats (JSON, YAML, XML, CSV) with validation and error handling" + ) + + parser.add_argument( + "--input-file-path", + help="input_file_path" + ) + parser.add_argument( + "--source-format", + help="source_format" + ) + parser.add_argument( + "--target-format", + help="target_format" + ) + parser.add_argument( + "--schema-path-optional", + help="schema_path (optional)" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = DataTransform() + + # Execute skill + result = skill.execute( + input_file_path=args.input_file_path, + source_format=args.source_format, + target_format=args.target_format, + schema_path_optional=args.schema_path_optional, + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/data.transform/skill.yaml b/skills/data.transform/skill.yaml new file mode 100644 index 0000000..7b47fdb --- /dev/null +++ b/skills/data.transform/skill.yaml @@ -0,0 +1,26 @@ +name: data.transform +version: 0.1.0 +description: Transform data between different formats (JSON, YAML, XML, CSV) with + validation and error handling +inputs: +- input_file_path +- source_format +- target_format +- schema_path (optional) +outputs: +- transformed_file +- transformation_report.json +status: active +permissions: +- filesystem:read +- filesystem:write +entrypoints: +- command: /data/transform + handler: data_transform.py + runtime: python + description: Transform data between different formats (JSON, YAML, XML, CSV) with + validation and error handling +artifact_metadata: + produces: + - type: transformed-data + - type: transformation-report diff --git a/skills/data.transform/test_data_transform.py b/skills/data.transform/test_data_transform.py new file mode 100644 index 0000000..2eb758c --- /dev/null +++ b/skills/data.transform/test_data_transform.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for data.transform + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.data_transform import data_transform + + +class TestDataTransform: + """Tests for DataTransform""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = data_transform.DataTransform() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["data_transform.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + data_transform.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Transform data between different formats (JSON, YA" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/docs.expand.glossary/SKILL.md b/skills/docs.expand.glossary/SKILL.md new file mode 100644 index 0000000..9a94424 --- /dev/null +++ b/skills/docs.expand.glossary/SKILL.md @@ -0,0 +1,261 @@ +# docs.expand.glossary + +**Version**: 0.1.0 +**Status**: active + +## Overview + +The `docs.expand.glossary` skill automatically discovers undocumented terms from Betty manifests and documentation, then enriches `glossary.md` with auto-generated definitions. This ensures comprehensive documentation coverage and helps maintain consistency across the Betty ecosystem. + +## Purpose + +- Extract field names and values from `skill.yaml` and `agent.yaml` manifests +- Scan markdown documentation for capitalized terms that may need definitions +- Identify gaps in the existing glossary +- Auto-generate definitions for common technical terms +- Update `glossary.md` with new entries organized alphabetically +- Emit JSON summary of changes for auditing + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `glossary_path` | string | No | `docs/glossary.md` | Path to the glossary file to expand | +| `base_dir` | string | No | Project root | Base directory to scan for manifests | +| `dry_run` | boolean | No | `false` | Preview changes without writing to file | +| `include_auto_generated` | boolean | No | `true` | Include auto-generated definitions | + +## Outputs + +| Name | Type | Description | +|------|------|-------------| +| `summary` | object | Summary with counts, file paths, and operation metadata | +| `new_definitions` | object | Dictionary mapping new terms to their definitions | +| `manifest_terms` | object | Categorized terms extracted from manifests | +| `skipped_terms` | array | Terms that were skipped (already documented or too common) | + +## What Gets Scanned + +### Manifest Files + +**skill.yaml fields:** +- `status` values (active, draft, deprecated, archived) +- `runtime` values (python, javascript, bash) +- `permissions` (filesystem:read, filesystem:write, network:http) +- Input/output `type` values +- `entrypoints` parameters + +**agent.yaml fields:** +- `reasoning_mode` (iterative, oneshot) +- `status` values +- `capabilities` +- Error handling strategies (on_validation_failure, etc.) +- Timeout and retry configurations + +### Documentation + +- Scans all `docs/*.md` files for capitalized multi-word phrases +- Identifies technical terms that may need glossary entries +- Filters out common words and overly generic terms + +## How It Works + +1. **Load Existing Glossary**: Parses `glossary.md` to identify already-documented terms +2. **Scan Manifests**: Recursively walks `skills/` and `agents/` directories for YAML files +3. **Extract Terms**: Collects field names, values, and configuration options from manifests +4. **Scan Docs**: Looks for capitalized terms in markdown documentation +5. **Generate Definitions**: Creates concise, accurate definitions for common technical terms +6. **Update Glossary**: Inserts new terms alphabetically into appropriate sections +7. **Report**: Returns JSON summary with all changes and statistics + +## Auto-Generated Definitions + +The skill includes predefined definitions for common terms: + +- **Status values**: active, draft, deprecated, archived +- **Runtimes**: python, javascript, bash +- **Permissions**: filesystem:read, filesystem:write, network:http +- **Reasoning modes**: iterative, oneshot +- **Types**: string, boolean, integer, object, array +- **Configuration**: max_retries, timeout_seconds, blocking, fuzzy +- **Modes**: dry_run, strict, overwrite + +For unknown terms, the skill can generate contextual definitions based on category and usage patterns. + +## Usage Examples + +### Basic Usage + +```bash +# Expand glossary with all undocumented terms +python glossary_expand.py + +# Preview changes without writing +python glossary_expand.py --dry-run + +# Use custom glossary location +python glossary_expand.py --glossary-path /path/to/glossary.md +``` + +### Programmatic Usage + +```python +from skills.docs.expand.glossary.glossary_expand import expand_glossary + +# Expand glossary +result = expand_glossary( + glossary_path="docs/glossary.md", + dry_run=False, + include_auto_generated=True +) + +# Check results +if result['ok']: + summary = result['details']['summary'] + print(f"Added {summary['new_terms_count']} new terms") + print(f"New terms: {summary['new_terms']}") +``` + +### Output Format + +#### Summary Mode (Default) + +``` +================================================================================ +GLOSSARY EXPANSION SUMMARY +================================================================================ + +Glossary: /home/user/betty/docs/glossary.md +Existing terms: 45 +New terms added: 8 +Scanned: 25 skills, 2 agents + +-------------------------------------------------------------------------------- +NEW TERMS: +-------------------------------------------------------------------------------- + +### Archived +A status indicating that a component has been retired and is no longer +maintained or available. + +### Dry Run +A mode that previews an operation without actually executing it or making +changes. + +### Handler +The script or function that implements the core logic of a skill or operation. + +... + +-------------------------------------------------------------------------------- + +Glossary updated successfully! + +================================================================================ +``` + +#### JSON Mode + +```json +{ + "ok": true, + "status": "success", + "timestamp": "2025-10-23T19:54:00Z", + "details": { + "summary": { + "glossary_path": "docs/glossary.md", + "existing_terms_count": 45, + "new_terms_count": 8, + "new_terms": ["Archived", "Dry Run", "Handler", ...], + "scanned_files": { + "skills": 25, + "agents": 2 + } + }, + "new_definitions": { + "Archived": "A status indicating...", + "Dry Run": "A mode that previews...", + ... + }, + "manifest_terms": { + "status": ["active", "draft", "deprecated", "archived"], + "runtime": ["python", "bash"], + "permissions": ["filesystem:read", "filesystem:write"] + } + } +} +``` + +## Integration + +### With CI/CD + +```yaml +# .github/workflows/docs-check.yml +- name: Check glossary completeness + run: | + python skills/docs.expand.glossary/glossary_expand.py --dry-run + # Fail if new terms found + if [ $? -eq 0 ]; then + echo "Glossary is complete" + else + echo "Missing glossary terms - run skill to update" + exit 1 + fi +``` + +### As a Hook + +Can be integrated as a pre-commit hook to ensure glossary stays current: + +```yaml +# .claude/hooks.yaml +- name: glossary-completeness-check + event: on_commit + command: python skills/docs.expand.glossary/glossary_expand.py --dry-run + blocking: false +``` + +## Skipped Terms + +The skill automatically skips: + +- Terms already in the glossary +- Common words (name, version, description, etc.) +- Generic types (string, boolean, file, path, etc.) +- Single-character or overly generic terms + +## Limitations + +- Auto-generated definitions may need manual refinement for domain-specific terms +- Complex or nuanced terms may require human review +- Alphabetical insertion may need manual adjustment for optimal organization +- Does not detect duplicate or inconsistent definitions + +## Future Enhancements + +- Detect and flag duplicate definitions +- Identify outdated or inconsistent glossary entries +- Generate contextual definitions using LLM analysis +- Support for multi-language glossaries +- Integration with documentation linting tools + +## Dependencies + +- `context.schema` - For validating manifest structure + +## Tags + +`documentation`, `glossary`, `automation`, `analysis`, `manifests` + +## Related Skills + +- `generate.docs` - Generate SKILL.md documentation from manifests +- `registry.query` - Query registries for specific terms and metadata +- `skill.define` - Define and register new skills + +## See Also + +- [Glossary](../../docs/glossary.md) - The Betty Framework glossary +- [Contributing](../../docs/contributing.md) - Documentation contribution guidelines +- [Developer Guide](../../docs/developer-guide.md) - Building and extending Betty diff --git a/skills/docs.expand.glossary/__init__.py b/skills/docs.expand.glossary/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/docs.expand.glossary/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/docs.expand.glossary/glossary_expand.py b/skills/docs.expand.glossary/glossary_expand.py new file mode 100755 index 0000000..b091aeb --- /dev/null +++ b/skills/docs.expand.glossary/glossary_expand.py @@ -0,0 +1,572 @@ +#!/usr/bin/env python3 +""" +glossary_expand.py - Implementation of the docs.expand.glossary Skill + +Extract undocumented terms from Betty manifests and docs, then enrich glossary.md +with new definitions. +""" + +import os +import sys +import json +import yaml +import re +from typing import Dict, Any, List, Set, 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 +from betty.errors import BettyError + +logger = setup_logger(__name__) + + +# Common field names found in manifests +SKILL_FIELDS = { + "name", "version", "description", "inputs", "outputs", "dependencies", + "entrypoints", "status", "tags", "runtime", "handler", "permissions", + "parameters", "command", "required", "type", "default" +} + +AGENT_FIELDS = { + "name", "version", "description", "capabilities", "skills_available", + "reasoning_mode", "context_requirements", "workflow_pattern", + "error_handling", "output", "status", "tags", "dependencies", + "max_retries", "timeout_seconds", "on_validation_failure", + "on_generation_failure", "on_compilation_failure" +} + +COMMAND_FIELDS = { + "name", "description", "execution", "parameters", "version", "status", + "tags", "delegate_to", "workflow", "agent", "skill" +} + +HOOK_FIELDS = { + "name", "description", "event", "command", "enabled", "blocking", + "timeout", "version", "status", "tags" +} + +# Terms that are already well-documented or common +SKIP_TERMS = { + "name", "version", "description", "true", "false", "string", "boolean", + "integer", "array", "object", "list", "dict", "file", "path", "url", + "id", "uuid", "timestamp", "date", "time", "json", "yaml", "xml" +} + + +def build_response( + ok: bool, + errors: Optional[List[str]] = None, + details: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: + """Build standardized response.""" + response: Dict[str, Any] = { + "ok": ok, + "status": "success" if ok else "failed", + "errors": errors or [], + "timestamp": datetime.now(timezone.utc).isoformat() + } + if details is not None: + response["details"] = details + return response + + +def load_glossary(glossary_path: str) -> Dict[str, str]: + """ + Load existing glossary and extract defined terms. + + Args: + glossary_path: Path to glossary.md + + Returns: + Dictionary mapping term names to their sections + """ + if not os.path.exists(glossary_path): + logger.warning(f"Glossary not found: {glossary_path}") + return {} + + terms = {} + with open(glossary_path, 'r') as f: + content = f.read() + + # Extract term headings (### Term Name) + pattern = r'^###\s+(.+)$' + matches = re.finditer(pattern, content, re.MULTILINE) + + for match in matches: + term = match.group(1).strip() + terms[term.lower()] = term + + logger.info(f"Loaded {len(terms)} existing glossary terms") + return terms + + +def scan_yaml_files(pattern: str, base_dir: str) -> List[Dict[str, Any]]: + """ + Scan YAML files matching pattern. + + Args: + pattern: File pattern (e.g., "skill.yaml", "agent.yaml") + base_dir: Base directory to search + + Returns: + List of parsed YAML data + """ + files = [] + for root, dirs, filenames in os.walk(base_dir): + for filename in filenames: + if filename == pattern: + file_path = os.path.join(root, filename) + try: + with open(file_path, 'r') as f: + data = yaml.safe_load(f) + if data: + data['_source_path'] = file_path + files.append(data) + except Exception as e: + logger.warning(f"Failed to parse {file_path}: {e}") + + logger.info(f"Scanned {len(files)} {pattern} files") + return files + + +def scan_markdown_files(docs_dir: str) -> List[str]: + """ + Scan markdown files for capitalized terms that might need definitions. + + Args: + docs_dir: Directory containing markdown files + + Returns: + List of potential terms found in docs + """ + terms = set() + + for file_path in Path(docs_dir).glob("*.md"): + try: + with open(file_path, 'r') as f: + content = f.read() + + # Find capitalized phrases (potential terms) + # Look for patterns like "Breaking Change", "Blocking Hook", etc. + pattern = r'\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b' + matches = re.finditer(pattern, content) + + for match in matches: + term = match.group(1) + # Filter out common words and single words + if len(term.split()) > 1 or term.lower() not in SKIP_TERMS: + terms.add(term) + + except Exception as e: + logger.warning(f"Failed to scan {file_path}: {e}") + + logger.info(f"Found {len(terms)} potential terms in docs") + return list(terms) + + +def extract_terms_from_manifests( + skills: List[Dict[str, Any]], + agents: List[Dict[str, Any]] +) -> Dict[str, List[str]]: + """ + Extract field names and values from manifests. + + Args: + skills: List of skill manifests + agents: List of agent manifests + + Returns: + Dictionary of term categories to terms + """ + terms = defaultdict(set) + + # Extract from skills + for skill in skills: + # Status values + if 'status' in skill: + terms['status'].add(skill['status']) + + # Runtime values + for ep in skill.get('entrypoints', []): + if 'runtime' in ep: + terms['runtime'].add(ep['runtime']) + if 'permissions' in ep: + for perm in ep['permissions']: + terms['permissions'].add(perm) + + # Input/output types + for input_def in skill.get('inputs', []): + if isinstance(input_def, dict) and 'type' in input_def: + terms['types'].add(input_def['type']) + + for output_def in skill.get('outputs', []): + if isinstance(output_def, dict) and 'type' in output_def: + terms['types'].add(output_def['type']) + + # Extract from agents + for agent in agents: + # Reasoning modes + if 'reasoning_mode' in agent: + terms['reasoning_mode'].add(agent['reasoning_mode']) + + # Status values + if 'status' in agent: + terms['status'].add(agent['status']) + + # Error handling strategies + error_handling = agent.get('error_handling', {}) + for key in error_handling: + if key.startswith('on_'): + terms['error_handling'].add(key) + + # Convert sets to sorted lists + return {k: sorted(v) for k, v in terms.items()} + + +def generate_definition(term: str, category: str, context: Dict[str, Any]) -> Optional[str]: + """ + Generate a glossary definition for a term. + + Args: + term: Term to define + category: Category of the term (e.g., 'status', 'runtime') + context: Additional context from manifests + + Returns: + Generated definition or None if unable to generate + """ + definitions = { + # Status values + 'active': 'A status indicating that a component is production-ready and available for use in workflows and operations.', + 'draft': 'A status indicating that a component is under development and not yet production-ready. Draft components are excluded from production operations.', + 'deprecated': 'A status indicating that a component is no longer recommended for use and may be removed in future versions.', + 'archived': 'A status indicating that a component has been retired and is no longer maintained or available.', + + # Runtime values + 'python': 'A runtime environment for executing Python-based skills and operations.', + 'javascript': 'A runtime environment for executing JavaScript/Node.js-based skills and operations.', + 'bash': 'A runtime environment for executing shell scripts and command-line operations.', + + # Permissions + 'filesystem:read': 'Permission to read files and directories from the filesystem.', + 'filesystem:write': 'Permission to write, modify, or delete files and directories.', + 'network:http': 'Permission to make HTTP/HTTPS network requests.', + 'network:all': 'Permission to make any network connections.', + + # Reasoning modes (already in glossary but we can check) + 'iterative': 'A reasoning mode where an agent can retry operations based on feedback, useful for tasks requiring refinement.', + 'oneshot': 'A reasoning mode where an agent executes once without retries, suitable for deterministic tasks.', + + # Types + 'string': 'A text value type.', + 'boolean': 'A true/false value type.', + 'integer': 'A whole number value type.', + 'object': 'A structured data type containing key-value pairs.', + 'array': 'A list of values.', + + # Error handling + 'on_validation_failure': 'Error handling strategy that defines actions to take when validation fails.', + 'on_generation_failure': 'Error handling strategy that defines actions to take when generation fails.', + 'on_compilation_failure': 'Error handling strategy that defines actions to take when compilation fails.', + + # Other common terms + 'max_retries': 'The maximum number of retry attempts allowed for an operation before failing.', + 'timeout_seconds': 'The maximum time in seconds that an operation is allowed to run before being terminated.', + 'blocking': 'A property indicating that an operation must complete (or fail) before subsequent operations can proceed.', + 'fuzzy': 'A matching mode that allows approximate string matching rather than exact matches.', + 'handler': 'The script or function that implements the core logic of a skill or operation.', + 'strict': 'A validation mode where warnings are treated as errors.', + 'dry_run': 'A mode that previews an operation without actually executing it or making changes.', + 'overwrite': 'An option to replace existing content rather than preserving or merging it.', + } + + # Return predefined definition if available + if term.lower() in definitions: + return definitions[term.lower()] + + # Generate contextual definitions based on category + if category == 'permissions': + parts = term.split(':') + if len(parts) == 2: + resource, action = parts + return f"Permission to {action} {resource} resources." + + return None + + +def update_glossary( + glossary_path: str, + new_terms: Dict[str, str], + dry_run: bool = False +) -> str: + """ + Update glossary.md with new term definitions. + + Args: + glossary_path: Path to glossary.md + new_terms: Dictionary mapping terms to definitions + dry_run: If True, don't write to file + + Returns: + Updated glossary content + """ + # Read existing glossary + with open(glossary_path, 'r') as f: + content = f.read() + + # Group terms by first letter + terms_by_letter = defaultdict(list) + for term, definition in sorted(new_terms.items()): + first_letter = term[0].upper() + terms_by_letter[first_letter].append((term, definition)) + + # Find insertion points and add new terms + lines = content.split('\n') + new_lines = [] + current_section = None + + for i, line in enumerate(lines): + new_lines.append(line) + + # Detect section headers (## A, ## B, etc.) + section_match = re.match(r'^##\s+([A-Z])\s*$', line) + if section_match: + current_section = section_match.group(1) + + # If we have new terms for this section, add them + if current_section in terms_by_letter: + # Find the right place to insert (alphabetically) + # For now, append at the end of the section + for term, definition in terms_by_letter[current_section]: + new_lines.append('') + new_lines.append(f'### {term}') + new_lines.append(definition) + + new_content = '\n'.join(new_lines) + + if not dry_run: + with open(glossary_path, 'w') as f: + f.write(new_content) + logger.info(f"Updated glossary with {len(new_terms)} new terms") + + return new_content + + +def expand_glossary( + glossary_path: Optional[str] = None, + base_dir: Optional[str] = None, + dry_run: bool = False, + include_auto_generated: bool = True +) -> Dict[str, Any]: + """ + Main function to expand glossary with undocumented terms. + + Args: + glossary_path: Path to glossary.md (default: docs/glossary.md) + base_dir: Base directory to scan (default: BASE_DIR) + dry_run: Preview changes without writing + include_auto_generated: Include auto-generated definitions + + Returns: + Result with new terms and summary + """ + # Set defaults + if base_dir is None: + base_dir = BASE_DIR + + if glossary_path is None: + glossary_path = os.path.join(base_dir, "docs", "glossary.md") + + logger.info(f"Expanding glossary at {glossary_path}") + + # Load existing glossary + existing_terms = load_glossary(glossary_path) + + # Scan manifests + skills = scan_yaml_files("skill.yaml", os.path.join(base_dir, "skills")) + agents = scan_yaml_files("agent.yaml", os.path.join(base_dir, "agents")) + + # Extract terms from manifests + manifest_terms = extract_terms_from_manifests(skills, agents) + + # Scan docs for additional terms + docs_dir = os.path.join(base_dir, "docs") + doc_terms = scan_markdown_files(docs_dir) + + # Find undocumented terms + new_terms = {} + skipped_terms = [] + + for category, terms in manifest_terms.items(): + for term in terms: + term_lower = term.lower() + + # Skip if already in glossary + if term_lower in existing_terms: + continue + + # Skip common terms + if term_lower in SKIP_TERMS: + skipped_terms.append(term) + continue + + # Generate definition + if include_auto_generated: + definition = generate_definition(term, category, { + 'category': category, + 'skills': skills, + 'agents': agents + }) + + if definition: + # Capitalize term name properly + term_name = term.title() if term.islower() else term + new_terms[term_name] = definition + else: + skipped_terms.append(term) + + # Update glossary + updated_content = None + if new_terms: + updated_content = update_glossary(glossary_path, new_terms, dry_run) + + # Build summary + summary = { + "glossary_path": glossary_path, + "existing_terms_count": len(existing_terms), + "new_terms_count": len(new_terms), + "new_terms": list(new_terms.keys()), + "skipped_terms_count": len(skipped_terms), + "scanned_files": { + "skills": len(skills), + "agents": len(agents) + }, + "dry_run": dry_run + } + + if dry_run and updated_content: + summary["preview"] = updated_content + + # Build detailed output + details = { + "summary": summary, + "new_definitions": new_terms, + "manifest_terms": manifest_terms, + "skipped_terms": skipped_terms[:20] # Limit to first 20 + } + + return build_response(ok=True, details=details) + + +def main(): + """Main CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Expand glossary.md with undocumented terms from manifests", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Expand glossary with new terms + glossary_expand.py + + # Preview changes without writing + glossary_expand.py --dry-run + + # Use custom glossary path + glossary_expand.py --glossary-path /path/to/glossary.md + + # Skip auto-generated definitions (only show what's missing) + glossary_expand.py --no-auto-generate + """ + ) + + parser.add_argument( + "--glossary-path", + help="Path to glossary.md file" + ) + parser.add_argument( + "--base-dir", + help="Base directory to scan for manifests" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Preview changes without writing to glossary" + ) + parser.add_argument( + "--no-auto-generate", + action="store_true", + help="Don't auto-generate definitions, only report missing terms" + ) + parser.add_argument( + "--format", + choices=["json", "summary"], + default="summary", + help="Output format" + ) + + args = parser.parse_args() + + try: + result = expand_glossary( + glossary_path=args.glossary_path, + base_dir=args.base_dir, + dry_run=args.dry_run, + include_auto_generated=not args.no_auto_generate + ) + + if args.format == "json": + print(json.dumps(result, indent=2)) + else: + # Pretty summary output + details = result["details"] + summary = details["summary"] + + print("\n" + "="*80) + print("GLOSSARY EXPANSION SUMMARY") + print("="*80) + print(f"\nGlossary: {summary['glossary_path']}") + print(f"Existing terms: {summary['existing_terms_count']}") + print(f"New terms added: {summary['new_terms_count']}") + print(f"Scanned: {summary['scanned_files']['skills']} skills, " + f"{summary['scanned_files']['agents']} agents") + + if summary['new_terms_count'] > 0: + print(f"\n{'-'*80}") + print("NEW TERMS:") + print(f"{'-'*80}") + for term in summary['new_terms']: + definition = details['new_definitions'][term] + print(f"\n### {term}") + print(definition) + print(f"\n{'-'*80}") + + if summary['dry_run']: + print("\n[DRY RUN] No changes written to glossary") + else: + print(f"\nGlossary updated successfully!") + + print("\n" + "="*80 + "\n") + + sys.exit(0 if result['ok'] else 1) + + except BettyError as e: + logger.error(f"Failed to expand glossary: {e}") + result = build_response(ok=False, errors=[str(e)]) + print(json.dumps(result, indent=2)) + sys.exit(1) + + except Exception as e: + logger.error(f"Unexpected error: {e}", exc_info=True) + result = build_response(ok=False, errors=[f"Unexpected error: {str(e)}"]) + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/docs.expand.glossary/skill.yaml b/skills/docs.expand.glossary/skill.yaml new file mode 100644 index 0000000..6736e13 --- /dev/null +++ b/skills/docs.expand.glossary/skill.yaml @@ -0,0 +1,86 @@ +name: docs.expand.glossary +version: 0.1.0 +description: >- + Extract undocumented terms from manifests and documentation, then enrich + glossary.md with auto-generated definitions. Scans skill.yaml, agent.yaml, + and markdown files to identify missing glossary entries. + +inputs: + - name: glossary_path + type: string + required: false + description: "Path to glossary.md file (default: docs/glossary.md)" + + - name: base_dir + type: string + required: false + description: "Base directory to scan for manifests (default: project root)" + + - name: dry_run + type: boolean + required: false + default: false + description: Preview changes without writing to glossary file + + - name: include_auto_generated + type: boolean + required: false + default: true + description: Include auto-generated definitions for common terms + +outputs: + - name: summary + type: object + description: Summary of glossary expansion including counts and file paths + + - name: new_definitions + type: object + description: Dictionary of new terms and their definitions + + - name: manifest_terms + type: object + description: Categorized terms extracted from manifests + + - name: skipped_terms + type: array + description: Terms that were skipped (already documented or too common) + +dependencies: + - context.schema + +entrypoints: + - command: /docs/expand/glossary + handler: glossary_expand.py + runtime: python + description: > + Scan manifests and docs for undocumented terms, then expand glossary.md + with new definitions. Supports dry-run mode for previewing changes. + parameters: + - name: glossary_path + type: string + required: false + description: Custom path to glossary.md + - name: base_dir + type: string + required: false + description: Custom base directory to scan + - name: dry_run + type: boolean + required: false + description: Preview changes without writing + - name: include_auto_generated + type: boolean + required: false + description: Include auto-generated definitions + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: + - documentation + - glossary + - automation + - analysis + - manifests diff --git a/skills/docs.lint.links/SKILL.md b/skills/docs.lint.links/SKILL.md new file mode 100644 index 0000000..118f3b8 --- /dev/null +++ b/skills/docs.lint.links/SKILL.md @@ -0,0 +1,313 @@ +# docs.lint.links + +## Overview + +**docs.lint.links** validates Markdown links to detect broken internal or external links, with optional autofix mode to correct common issues. + +## Purpose + +This skill helps maintain documentation quality by: +- Scanning all `.md` files in a repository +- Detecting broken external links (404s and other HTTP errors) +- Detecting broken internal links (relative paths that don't resolve) +- Providing suggested fixes for common issues +- Automatically fixing case mismatches and `.md` extension issues + +## Usage + +### Basic Usage + +```bash +python skills/docs.lint.links/docs_link_lint.py [root_dir] [options] +``` + +### Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `root_dir` | string | No | `.` | Root directory to search for Markdown files | +| `--no-external` | boolean | No | false | Skip checking external links (faster) | +| `--autofix` | boolean | No | false | Automatically fix common issues (case mismatches, .md extension issues) | +| `--timeout` | integer | No | `10` | Timeout for external link checks in seconds | +| `--exclude` | string | No | - | Comma-separated list of patterns to exclude (e.g., 'node_modules,.git') | +| `--output` | string | No | `json` | Output format (json or text) | + +## Outputs + +| Output | Type | Description | +|--------|------|-------------| +| `lint_results` | object | JSON object containing link validation results with issues and statistics | +| `issues` | array | Array of link issues found, each with file, line, link, issue type, and suggested fix | +| `summary` | object | Summary statistics including files checked, issues found, and fixes applied | + +## Usage Examples + +### Example 1: Basic Link Validation + +Check all markdown files in the current directory for broken links: + +```bash +python skills/docs.lint.links/docs_link_lint.py +``` + +### Example 2: Skip External Link Checks + +Check only internal links (much faster): + +```bash +python skills/docs.lint.links/docs_link_lint.py --no-external +``` + +### Example 3: Auto-fix Common Issues + +Automatically fix case mismatches and `.md` extension issues: + +```bash +python skills/docs.lint.links/docs_link_lint.py --autofix +``` + +### Example 4: Check Specific Directory + +Check markdown files in the `docs` directory: + +```bash +python skills/docs.lint.links/docs_link_lint.py docs/ +``` + +### Example 5: Exclude Patterns + +Exclude certain directories from checking: + +```bash +python skills/docs.lint.links/docs_link_lint.py --exclude "node_modules,vendor,.venv" +``` + +### Example 6: Text Output + +Get human-readable text output instead of JSON: + +```bash +python skills/docs.lint.links/docs_link_lint.py --output text +``` + +### Example 7: Custom Timeout + +Use a longer timeout for external link checks: + +```bash +python skills/docs.lint.links/docs_link_lint.py --timeout 30 +``` + +## Output Format + +### JSON Output (Default) + +```json +{ + "status": "success", + "summary": { + "files_checked": 42, + "files_with_issues": 3, + "total_issues": 5, + "autofix_enabled": false, + "total_fixes_applied": 0 + }, + "issues": [ + { + "file": "docs/api.md", + "line": 15, + "link": "../README.MD", + "issue_type": "internal_broken", + "message": "File not found: ../README.MD (found case mismatch: README.md)", + "suggested_fix": "../README.md" + }, + { + "file": "docs/guide.md", + "line": 23, + "link": "https://example.com/missing", + "issue_type": "external_broken", + "message": "External link is broken: HTTP 404" + } + ] +} +``` + +### Text Output + +``` +Markdown Link Lint Results +================================================== +Files checked: 42 +Files with issues: 3 +Total issues: 5 + +Issues found: +-------------------------------------------------- + +docs/api.md:15 + Link: ../README.MD + Issue: File not found: ../README.MD (found case mismatch: README.md) + Suggested fix: ../README.md + +docs/guide.md:23 + Link: https://example.com/missing + Issue: External link is broken: HTTP 404 +``` + +## Issue Types + +### Internal Broken Links + +These are relative file paths that don't resolve: + +- **Case mismatches**: `README.MD` when file is `README.md` +- **Missing `.md` extension**: `guide` when file is `guide.md` +- **Extra `.md` extension**: `file.md` when file is `file` +- **File not found**: Path doesn't exist in the repository + +### External Broken Links + +These are HTTP/HTTPS URLs that return errors: + +- **404 Not Found**: Page doesn't exist +- **403 Forbidden**: Access denied +- **500+ Server Errors**: Server-side issues +- **Timeout**: Server didn't respond in time +- **Network errors**: DNS failures, connection refused, etc. + +## Autofix Behavior + +When `--autofix` is enabled, the skill will automatically correct: + +1. **Case mismatches**: If a link uses wrong case but a case-insensitive match exists +2. **Missing `.md` extension**: If a link is missing `.md` but the file exists with it +3. **Extra `.md` extension**: If a link has `.md` but the file exists without it + +The autofix preserves: +- Anchor fragments (e.g., `#section`) +- Query parameters (e.g., `?version=1.0`) + +**Note**: Autofix modifies files in place. It's recommended to use version control or create backups before using this option. + +## Link Detection + +The skill detects the following link formats: + +1. **Standard markdown links**: `[text](url)` +2. **Angle bracket URLs**: `` +3. **Reference-style links**: `[text][ref]` with `[ref]: url` definitions +4. **Implicit reference links**: `[text][]` using text as reference + +## Excluded Patterns + +By default, the following patterns are excluded from scanning: + +- `.git/` +- `node_modules/` +- `.venv/` and `venv/` +- `__pycache__/` + +Additional patterns can be excluded using the `--exclude` parameter. + +## Integration Examples + +### Use in CI/CD + +Add to your CI pipeline to catch broken links: + +```yaml +# .github/workflows/docs-lint.yml +name: Documentation Link Check + +on: [push, pull_request] + +jobs: + lint-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Check documentation links + run: | + python skills/docs.lint.links/docs_link_lint.py --no-external +``` + +### Use with Pre-commit Hook + +Add to `.git/hooks/pre-commit`: + +```bash +#!/bin/bash +python skills/docs.lint.links/docs_link_lint.py --no-external --output text +if [ $? -ne 0 ]; then + echo "Documentation has broken links. Please fix before committing." + exit 1 +fi +``` + +### Use in Documentation Workflow + +```yaml +# workflows/documentation.yaml +steps: + - skill: docs.lint.links + args: + - "docs/" + - "--autofix" + - skill: docs.lint.links + args: + - "docs/" + - "--output=text" +``` + +## Performance Considerations + +### External Link Checking + +Checking external links can be slow because: +- Each link requires an HTTP request +- Some servers may rate-limit or block automated requests +- Network latency and timeouts add up + +**Recommendations**: +- Use `--no-external` for fast local checks +- Use `--timeout` to adjust timeout for slow networks +- Run external checks less frequently (e.g., nightly builds) + +### Large Repositories + +For repositories with many markdown files: +- Use `--exclude` to skip irrelevant directories +- Consider checking specific subdirectories instead of the entire repo +- The skill automatically skips common directories like `node_modules` + +## Error Handling + +The skill returns: +- Exit code `0` if no broken links are found +- Exit code `1` if broken links are found or an error occurs + +This makes it suitable for use in CI/CD pipelines and pre-commit hooks. + +## Dependencies + +_No external dependencies_ + +All functionality uses Python standard library modules: +- `re` - Regular expression matching for link extraction +- `urllib` - HTTP requests for external link checking +- `pathlib` - File system operations +- `json` - JSON output formatting + +## Tags + +`documentation`, `linting`, `validation`, `links`, `markdown` + +## See Also + +- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model +- [Skills Framework](../../docs/skills-framework.md) - Betty skills framework +- [generate.docs](../generate.docs/SKILL.md) - Generate documentation from manifests + +## Version + +**0.1.0** - Initial implementation with link validation and autofix support diff --git a/skills/docs.lint.links/__init__.py b/skills/docs.lint.links/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/docs.lint.links/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/docs.lint.links/docs_link_lint.py b/skills/docs.lint.links/docs_link_lint.py new file mode 100755 index 0000000..7409209 --- /dev/null +++ b/skills/docs.lint.links/docs_link_lint.py @@ -0,0 +1,609 @@ +#!/usr/bin/env python3 +""" +docs_link_lint.py - Implementation of the docs.lint.links Skill. + +Validates Markdown links to detect broken internal or external links. +""" + +import json +import os +import re +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple +from urllib.parse import urlparse +from urllib.request import Request, urlopen +from urllib.error import HTTPError, URLError + +# Ensure project root on path for betty imports when executed directly + +from betty.errors import BettyError # noqa: E402 +from betty.logging_utils import setup_logger # noqa: E402 + +logger = setup_logger(__name__) + +# Regex patterns for finding links in markdown +# Matches [text](url) format +MARKDOWN_LINK_PATTERN = re.compile(r'\[([^\]]+)\]\(([^)]+)\)') +# Matches format +ANGLE_LINK_PATTERN = re.compile(r'<(https?://[^>]+)>') +# Matches reference-style links [text][ref] +REFERENCE_LINK_PATTERN = re.compile(r'\[([^\]]+)\]\[([^\]]*)\]') +# Matches reference definitions [ref]: url +REFERENCE_DEF_PATTERN = re.compile(r'^\[([^\]]+)\]:\s+(.+)$', re.MULTILINE) + + +class LinkIssue: + """Represents a broken or problematic link.""" + + def __init__( + self, + file: str, + line: int, + link: str, + issue_type: str, + message: str, + suggested_fix: Optional[str] = None + ): + self.file = file + self.line = line + self.link = link + self.issue_type = issue_type + self.message = message + self.suggested_fix = suggested_fix + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for JSON output.""" + result = { + "file": self.file, + "line": self.line, + "link": self.link, + "issue_type": self.issue_type, + "message": self.message + } + if self.suggested_fix: + result["suggested_fix"] = self.suggested_fix + return result + + +def find_markdown_files(root_dir: str, exclude_patterns: Optional[List[str]] = None) -> List[Path]: + """ + Find all .md files in the directory tree. + + Args: + root_dir: Root directory to search + exclude_patterns: List of path patterns to exclude (e.g., 'node_modules', '.git') + + Returns: + List of Path objects for markdown files + """ + exclude_patterns = exclude_patterns or ['.git', 'node_modules', '.venv', 'venv', '__pycache__'] + md_files = [] + + root_path = Path(root_dir).resolve() + + for path in root_path.rglob('*.md'): + # Skip excluded directories + if any(excluded in path.parts for excluded in exclude_patterns): + continue + md_files.append(path) + + logger.info(f"Found {len(md_files)} markdown files") + return md_files + + +def is_in_code_block(line: str) -> bool: + """ + Check if a line contains inline code that might contain false positive links. + + Args: + line: Line to check + + Returns: + True if we should skip this line for link extraction + """ + # Count backticks - if odd number, we're likely inside inline code + # This is a simple heuristic + backtick_count = line.count('`') + + # If we have backticks, we need to be more careful + # For simplicity, we'll extract the content outside of backticks + return False # We'll handle this differently + + +def extract_links_from_markdown(content: str) -> List[Tuple[int, str, str]]: + """ + Extract all links from markdown content. + + Args: + content: Markdown file content + + Returns: + List of tuples: (line_number, link_text, link_url) + """ + lines = content.split('\n') + links = [] + + # First, extract reference definitions + references = {} + for match in REFERENCE_DEF_PATTERN.finditer(content): + ref_name = match.group(1).lower() + ref_url = match.group(2).strip() + references[ref_name] = ref_url + + # Track if we're in a code block + in_code_block = False + + # Process each line + for line_num, line in enumerate(lines, start=1): + # Check for code block delimiters + if line.strip().startswith('```'): + in_code_block = not in_code_block + continue + + # Skip lines inside code blocks + if in_code_block: + continue + + # Remove inline code blocks from the line before processing + # This prevents false positives from code examples + processed_line = re.sub(r'`[^`]+`', '', line) + + # Find standard markdown links [text](url) + for match in MARKDOWN_LINK_PATTERN.finditer(processed_line): + # Check if this match is actually in the original line + # (not removed by our inline code filter) + match_pos = processed_line.find(match.group(0)) + if match_pos >= 0: + text = match.group(1) + url = match.group(2) + links.append((line_num, text, url)) + + # Find angle bracket links + for match in ANGLE_LINK_PATTERN.finditer(processed_line): + url = match.group(1) + links.append((line_num, url, url)) + + # Find reference-style links [text][ref] or [text][] + for match in REFERENCE_LINK_PATTERN.finditer(processed_line): + text = match.group(1) + ref = match.group(2) if match.group(2) else text + ref_lower = ref.lower() + if ref_lower in references: + url = references[ref_lower] + links.append((line_num, text, url)) + + return links + + +def is_external_link(url: str) -> bool: + """Check if a URL is external (http/https).""" + return url.startswith('http://') or url.startswith('https://') + + +def check_external_link(url: str, timeout: int = 10) -> Optional[str]: + """ + Check if an external URL is accessible. + + Args: + url: URL to check + timeout: Timeout in seconds + + Returns: + Error message if link is broken, None if OK + """ + try: + # Create request with a user agent to avoid 403s from some sites + req = Request( + url, + headers={ + 'User-Agent': 'Betty/1.0 (Link Checker)', + 'Accept': '*/*' + } + ) + + with urlopen(req, timeout=timeout) as response: + if response.status >= 400: + return f"HTTP {response.status}" + return None + + except HTTPError as e: + return f"HTTP {e.code}" + except URLError as e: + return f"URL Error: {e.reason}" + except Exception as e: + return f"Error: {str(e)}" + + +def resolve_relative_path(md_file_path: Path, relative_url: str) -> Path: + """ + Resolve a relative URL from a markdown file. + + Args: + md_file_path: Path to the markdown file containing the link + relative_url: Relative URL/path from the link + + Returns: + Resolved absolute path + """ + # Remove anchor/hash fragment + url_without_anchor = relative_url.split('#')[0] + + if not url_without_anchor: + # Just an anchor to current file + return md_file_path + + # Resolve relative to the markdown file's directory + base_dir = md_file_path.parent + resolved = (base_dir / url_without_anchor).resolve() + + return resolved + + +def check_internal_link( + md_file_path: Path, + relative_url: str, + root_dir: Path +) -> Tuple[Optional[str], Optional[str]]: + """ + Check if an internal link is valid. + + Args: + md_file_path: Path to the markdown file containing the link + relative_url: Relative URL from the link + root_dir: Repository root directory + + Returns: + Tuple of (error_message, suggested_fix) + """ + # Remove query string and anchor + clean_url = relative_url.split('?')[0].split('#')[0] + + if not clean_url: + # Just an anchor or query, assume valid + return None, None + + resolved = resolve_relative_path(md_file_path, clean_url) + + # Check if file exists + if resolved.exists(): + return None, None + + # File doesn't exist - try to suggest fixes + error_msg = f"File not found: {relative_url}" + suggested_fix = None + + # Try case-insensitive match + if resolved.parent.exists(): + for file in resolved.parent.iterdir(): + if file.name.lower() == resolved.name.lower(): + relative_to_md = os.path.relpath(file, md_file_path.parent) + suggested_fix = relative_to_md + error_msg += f" (found case mismatch: {file.name})" + break + + # Try without .md extension if it has one + if not suggested_fix and clean_url.endswith('.md'): + url_without_ext = clean_url[:-3] + resolved_without_ext = resolve_relative_path(md_file_path, url_without_ext) + if resolved_without_ext.exists(): + relative_to_md = os.path.relpath(resolved_without_ext, md_file_path.parent) + suggested_fix = relative_to_md + error_msg += f" (file exists without .md extension)" + + # Try adding .md extension if it doesn't have one + if not suggested_fix and not clean_url.endswith('.md'): + url_with_ext = clean_url + '.md' + resolved_with_ext = resolve_relative_path(md_file_path, url_with_ext) + if resolved_with_ext.exists(): + relative_to_md = os.path.relpath(resolved_with_ext, md_file_path.parent) + suggested_fix = relative_to_md + error_msg += f" (file exists with .md extension)" + + return error_msg, suggested_fix + + +def lint_markdown_file( + md_file: Path, + root_dir: Path, + check_external: bool = True, + external_timeout: int = 10 +) -> List[LinkIssue]: + """ + Lint a single markdown file for broken links. + + Args: + md_file: Path to markdown file + root_dir: Repository root directory + check_external: Whether to check external links + external_timeout: Timeout for external link checks + + Returns: + List of LinkIssue objects + """ + issues = [] + + try: + content = md_file.read_text(encoding='utf-8') + except Exception as e: + logger.warning(f"Could not read {md_file}: {e}") + return issues + + links = extract_links_from_markdown(content) + + for line_num, link_text, url in links: + # Skip empty URLs + if not url or url.strip() == '': + continue + + # Skip mailto and other special schemes + if url.startswith('mailto:') or url.startswith('tel:'): + continue + + relative_path = os.path.relpath(md_file, root_dir) + + if is_external_link(url): + if check_external: + logger.debug(f"Checking external link: {url}") + error = check_external_link(url, timeout=external_timeout) + if error: + issues.append(LinkIssue( + file=relative_path, + line=line_num, + link=url, + issue_type="external_broken", + message=f"External link is broken: {error}" + )) + else: + # Internal link + logger.debug(f"Checking internal link: {url}") + error, suggested_fix = check_internal_link(md_file, url, root_dir) + if error: + issues.append(LinkIssue( + file=relative_path, + line=line_num, + link=url, + issue_type="internal_broken", + message=error, + suggested_fix=suggested_fix + )) + + return issues + + +def autofix_markdown_file( + md_file: Path, + root_dir: Path +) -> Tuple[int, List[str]]: + """ + Automatically fix common link issues in a markdown file. + + Args: + md_file: Path to markdown file + root_dir: Repository root directory + + Returns: + Tuple of (number_of_fixes, list_of_fix_descriptions) + """ + try: + content = md_file.read_text(encoding='utf-8') + except Exception as e: + logger.warning(f"Could not read {md_file}: {e}") + return 0, [] + + original_content = content + links = extract_links_from_markdown(content) + fixes = [] + fix_count = 0 + + for line_num, link_text, url in links: + if is_external_link(url): + continue + + # Check if internal link is broken + error, suggested_fix = check_internal_link(md_file, url, root_dir) + + if error and suggested_fix: + # Apply the fix + # Preserve any anchor/hash + anchor = '' + if '#' in url: + anchor = '#' + url.split('#', 1)[1] + + new_url = suggested_fix + anchor + + # Replace in content + content = content.replace(f']({url})', f']({new_url})') + fix_count += 1 + fixes.append(f"Line {line_num}: {url} -> {new_url}") + + # Write back if changes were made + if fix_count > 0: + try: + md_file.write_text(content, encoding='utf-8') + logger.info(f"Applied {fix_count} fixes to {md_file}") + except Exception as e: + logger.error(f"Could not write fixes to {md_file}: {e}") + return 0, [] + + return fix_count, fixes + + +def lint_all_markdown( + root_dir: str, + check_external: bool = True, + autofix: bool = False, + external_timeout: int = 10, + exclude_patterns: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Lint all markdown files in a directory. + + Args: + root_dir: Root directory to search + check_external: Whether to check external links (can be slow) + autofix: Whether to automatically fix common issues + external_timeout: Timeout for external link checks + exclude_patterns: Patterns to exclude from search + + Returns: + Result dictionary with issues and statistics + """ + root_path = Path(root_dir).resolve() + md_files = find_markdown_files(root_dir, exclude_patterns) + + all_issues = [] + all_fixes = [] + files_checked = 0 + files_with_issues = 0 + total_fixes = 0 + + for md_file in md_files: + files_checked += 1 + + if autofix: + fix_count, fixes = autofix_markdown_file(md_file, root_path) + total_fixes += fix_count + if fixes: + relative_path = os.path.relpath(md_file, root_path) + all_fixes.append({ + "file": relative_path, + "fixes": fixes + }) + + # Check for issues (after autofix if enabled) + issues = lint_markdown_file( + md_file, + root_path, + check_external=check_external, + external_timeout=external_timeout + ) + + if issues: + files_with_issues += 1 + all_issues.extend(issues) + + result = { + "status": "success", + "summary": { + "files_checked": files_checked, + "files_with_issues": files_with_issues, + "total_issues": len(all_issues), + "autofix_enabled": autofix, + "total_fixes_applied": total_fixes + }, + "issues": [issue.to_dict() for issue in all_issues] + } + + if autofix and all_fixes: + result["fixes"] = all_fixes + + return result + + +def main(argv: Optional[List[str]] = None) -> int: + """Entry point for CLI execution.""" + import argparse + + parser = argparse.ArgumentParser( + description="Lint Markdown files to detect broken internal or external links" + ) + parser.add_argument( + "root_dir", + nargs='?', + default='.', + help="Root directory to search for Markdown files (default: current directory)" + ) + parser.add_argument( + "--no-external", + action="store_true", + help="Skip checking external links (faster)" + ) + parser.add_argument( + "--autofix", + action="store_true", + help="Automatically fix common issues (case, .md extension)" + ) + parser.add_argument( + "--timeout", + type=int, + default=10, + help="Timeout for external link checks in seconds (default: 10)" + ) + parser.add_argument( + "--exclude", + type=str, + help="Comma-separated list of patterns to exclude (e.g., 'node_modules,.git')" + ) + parser.add_argument( + "--output", + type=str, + choices=['json', 'text'], + default='json', + help="Output format (default: json)" + ) + + args = parser.parse_args(argv) + + exclude_patterns = None + if args.exclude: + exclude_patterns = [p.strip() for p in args.exclude.split(',')] + + try: + result = lint_all_markdown( + root_dir=args.root_dir, + check_external=not args.no_external, + autofix=args.autofix, + external_timeout=args.timeout, + exclude_patterns=exclude_patterns + ) + + if args.output == 'json': + print(json.dumps(result, indent=2)) + else: + # Text output + summary = result['summary'] + print(f"Markdown Link Lint Results") + print(f"=" * 50) + print(f"Files checked: {summary['files_checked']}") + print(f"Files with issues: {summary['files_with_issues']}") + print(f"Total issues: {summary['total_issues']}") + + if summary['autofix_enabled']: + print(f"Fixes applied: {summary['total_fixes_applied']}") + + if result['issues']: + print(f"\nIssues found:") + print(f"-" * 50) + for issue in result['issues']: + print(f"\n{issue['file']}:{issue['line']}") + print(f" Link: {issue['link']}") + print(f" Issue: {issue['message']}") + if issue.get('suggested_fix'): + print(f" Suggested fix: {issue['suggested_fix']}") + else: + print("\n✓ No issues found!") + + # Return non-zero if issues found + return 1 if result['issues'] else 0 + + except BettyError as e: + logger.error(f"Linting failed: {e}") + result = { + "status": "error", + "error": str(e) + } + print(json.dumps(result, indent=2)) + return 1 + except Exception as e: + logger.exception("Unexpected error during linting") + result = { + "status": "error", + "error": str(e) + } + print(json.dumps(result, indent=2)) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/docs.lint.links/skill.yaml b/skills/docs.lint.links/skill.yaml new file mode 100644 index 0000000..2fff60c --- /dev/null +++ b/skills/docs.lint.links/skill.yaml @@ -0,0 +1,97 @@ +name: docs.lint.links +version: 0.1.0 +description: > + Validates Markdown links to detect broken internal or external links, + with optional autofix mode to correct common issues. + +inputs: + - name: root_dir + type: string + required: false + default: "." + description: "Root directory to search for Markdown files (default: current directory)" + + - name: no_external + type: boolean + required: false + default: false + description: "Skip checking external links (faster)" + + - name: autofix + type: boolean + required: false + default: false + description: "Automatically fix common issues (case mismatches, .md extension issues)" + + - name: timeout + type: integer + required: false + default: 10 + description: "Timeout for external link checks in seconds" + + - name: exclude + type: string + required: false + description: "Comma-separated list of patterns to exclude (e.g., 'node_modules,.git')" + + - name: output + type: string + required: false + default: "json" + description: "Output format (json or text)" + +outputs: + - name: lint_results + type: object + description: "JSON object containing link validation results with issues and statistics" + + - name: issues + type: array + description: "Array of link issues found, each with file, line, link, issue type, and suggested fix" + + - name: summary + type: object + description: "Summary statistics including files checked, issues found, and fixes applied" + +dependencies: [] + +status: active + +entrypoints: + - command: /docs/lint/links + handler: docs_link_lint.py + runtime: python + description: > + Scan all Markdown files and detect broken internal or external links. + parameters: + - name: root_dir + type: string + required: false + description: "Root directory to search (default: current directory)" + - name: no_external + type: boolean + required: false + description: "Skip checking external links" + - name: autofix + type: boolean + required: false + description: "Automatically fix common issues" + - name: timeout + type: integer + required: false + description: "Timeout for external link checks in seconds" + - name: exclude + type: string + required: false + description: "Comma-separated exclusion patterns" + - name: output + type: string + required: false + description: "Output format (json or text)" + +permissions: + - filesystem:read + - filesystem:write + - network + +tags: [documentation, linting, validation, links, markdown] diff --git a/skills/docs.sync.pluginmanifest/SKILL.md b/skills/docs.sync.pluginmanifest/SKILL.md new file mode 100644 index 0000000..dbb0d1d --- /dev/null +++ b/skills/docs.sync.pluginmanifest/SKILL.md @@ -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 diff --git a/skills/docs.sync.pluginmanifest/__init__.py b/skills/docs.sync.pluginmanifest/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/docs.sync.pluginmanifest/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/docs.sync.pluginmanifest/plugin_manifest_sync.py b/skills/docs.sync.pluginmanifest/plugin_manifest_sync.py new file mode 100755 index 0000000..34daa53 --- /dev/null +++ b/skills/docs.sync.pluginmanifest/plugin_manifest_sync.py @@ -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() diff --git a/skills/docs.sync.pluginmanifest/skill.yaml b/skills/docs.sync.pluginmanifest/skill.yaml new file mode 100644 index 0000000..1773830 --- /dev/null +++ b/skills/docs.sync.pluginmanifest/skill.yaml @@ -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 diff --git a/skills/docs.sync.readme/SKILL.md b/skills/docs.sync.readme/SKILL.md new file mode 100644 index 0000000..54e0047 --- /dev/null +++ b/skills/docs.sync.readme/SKILL.md @@ -0,0 +1,490 @@ +--- +name: Documentation README Sync +description: Automatically regenerate README.md from Betty Framework registries +--- + +# docs.sync.readme + +## Overview + +**docs.sync.readme** is the documentation synchronization tool that regenerates the top-level `README.md` to reflect all current registered skills and agents. It ensures that the README stays in sync with the actual state of the Betty Framework by pulling from registry files. + +## Purpose + +Automates the maintenance of `README.md` to keep documentation accurate and up-to-date with: +- **Skill Registry** (`registry/skills.json`) – All registered skills +- **Agent Registry** (`registry/agents.json`) – All registered agents + +This eliminates manual editing of the README and prevents documentation drift as skills and agents are added, modified, or removed. + +## What It Does + +1. **Reads Registries**: Loads `skills.json` and `agents.json` +2. **Categorizes Skills**: Groups skills by tag/category: + - Foundation (skill.*, registry.*, workflow.*) + - API Development (api.*) + - Infrastructure (agents, commands, hooks, policy) + - Governance (policy, audit) +3. **Updates Sections**: + - Current Core Skills table with categorized skills + - Agents documentation links + - Skills documentation references +4. **Maintains Style**: Preserves README tone, formatting, and structure +5. **Generates Report**: Creates sync report with statistics + +## Usage + +### Basic Usage + +```bash +python skills/docs.sync.readme/readme_sync.py +``` + +No arguments required - reads from standard registry locations. + +### Via Betty CLI + +```bash +/docs/sync/readme +``` + +### Expected Registry Structure + +``` +betty/ +├── registry/ +│ ├── skills.json # Skills registry +│ └── agents.json # Agents registry +└── README.md # File to update +``` + +## Behavior + +### 1. Registry Loading + +Reads JSON files from: +- `registry/skills.json` – Skills registry +- `registry/agents.json` – Agents registry + +If a registry file is missing, logs a warning and continues with empty data. + +### 2. Skill Categorization + +**Foundation Skills**: +- Matches: `skill.*`, `registry.*`, `workflow.*` +- Examples: `skill.create`, `workflow.compose` + +**API Development Skills**: +- Matches: `api.*` or tags: `api`, `openapi`, `asyncapi` +- Examples: `api.define`, `api.validate` + +**Infrastructure Skills**: +- Matches tags: `agents`, `command`, `hook`, `policy`, `plugin` +- Examples: `agent.define`, `hook.register`, `plugin.sync` + +**Governance Skills**: +- Matches tags: `governance`, `policy`, `audit` +- Examples: `policy.enforce`, `audit.log` + +Only **active** skills are included. Test skills (starting with `test.`) are filtered out. + +### 3. Skills Section Update + +Replaces the "## 🧩 Current Core Skills" section with: + +```markdown +## 🧩 Current Core Skills + +Betty's self-referential "kernel" of skills bootstraps the rest of the system: + +### Foundation Skills + +| Skill | Purpose | +|--------|----------| +| **skill.create** | Generates a new Betty Framework Skill directory and manifest. | +| **skill.define** | Validates and registers skill manifests (.skill.yaml) for the Betty Framework. | +| **registry.update** | Updates the Betty Framework Skill Registry by adding or modifying entries. | + +### API Development Skills + +| Skill | Purpose | +|--------|----------| +| **api.define** | Create OpenAPI and AsyncAPI specifications from templates | +| **api.validate** | Validate OpenAPI and AsyncAPI specifications against enterprise guidelines | + +### Infrastructure Skills + +| Skill | Purpose | +|--------|----------| +| **agent.define** | Validates and registers agent manifests for the Betty Framework. | +| **hook.define** | Create and register validation hooks for Claude Code | + +These skills form the baseline for an **AI-native SDLC** where creation, validation, registration, and orchestration are themselves skills. +``` + +### 4. Agents Section Update + +Updates the "### Agents Documentation" subsection with current agents: + +```markdown +### Agents Documentation + +Each agent has a `README.md` in its directory: +* [api.designer](agents/api.designer/README.md) — Design RESTful APIs following enterprise guidelines with iterative refinement +* [api.analyzer](agents/api.analyzer/README.md) — Analyze API specifications for backward compatibility and breaking changes +``` + +Includes both `active` and `draft` agents. + +### 5. Report Generation + +Creates `sync_report.json` with statistics: + +```json +{ + "skills_by_category": { + "foundation": 5, + "api": 4, + "infrastructure": 9, + "governance": 1 + }, + "total_skills": 19, + "agents_count": 2, + "timestamp": "2025-10-23T20:30:00.123456+00:00" +} +``` + +## Outputs + +### Success Response + +```json +{ + "ok": true, + "status": "success", + "readme_path": "/home/user/betty/README.md", + "report": { + "skills_by_category": { + "foundation": 5, + "api": 4, + "infrastructure": 9, + "governance": 1 + }, + "total_skills": 19, + "agents_count": 2, + "timestamp": "2025-10-23T20:30:00.123456+00:00" + } +} +``` + +### Failure Response + +```json +{ + "ok": false, + "status": "failed", + "error": "README.md not found at /home/user/betty/README.md" +} +``` + +## What Gets Updated + +### ✅ Updated Sections + +- **Current Core Skills** (categorized tables) +- **Agents Documentation** (agent links list) +- Skills documentation references + +### ❌ Not Modified + +- Mission and inspiration +- Purpose and scope +- Repository structure +- Design principles +- Roadmap +- Contributing guidelines +- Requirements + +The skill only updates specific documentation sections while preserving all other README content. + +## Examples + +### Example 1: Sync After Adding New Skills + +**Scenario**: You've added several new skills and want to update the README + +```bash +# Create and register new skills +/skill/create data.transform "Transform data between formats" +/skill/define skills/data.transform/skill.yaml + +/skill/create telemetry.report "Generate telemetry reports" +/skill/define skills/telemetry.report/skill.yaml + +# Sync README to include new skills +/docs/sync/readme +``` + +**Output**: +``` +INFO: Starting README.md sync from registries... +INFO: Loading registry files... +INFO: Generating updated README content... +INFO: ✅ Updated README.md +INFO: - Foundation skills: 5 +INFO: - API skills: 4 +INFO: - Infrastructure skills: 11 +INFO: - Governance skills: 1 +INFO: - Total active skills: 21 +INFO: - Agents: 2 +``` + +### Example 2: Sync After Adding New Agent + +**Scenario**: A new agent has been registered and needs to appear in README + +```bash +# Define new agent +/agent/define agents/workflow.optimizer/agent.yaml + +# Sync README +/docs/sync/readme +``` + +The new agent will appear in the "### Agents Documentation" section. + +### Example 3: Automated Sync in Workflow + +**Scenario**: Include README sync as a workflow step after registering skills + +```yaml +# workflows/skill_release.yaml +steps: + - skill: skill.define + args: ["skills/new.skill/skill.yaml"] + + - skill: plugin.sync + args: [] + + - skill: docs.sync.readme + args: [] +``` + +This ensures README, plugin.yaml, and registries stay in sync. + +## Integration + +### With skill.define + +After defining skills, sync the README: + +```bash +/skill/define skills/my.skill/skill.yaml +/docs/sync/readme +``` + +### With agent.define + +After defining agents, sync the README: + +```bash +/agent/define agents/my.agent/agent.yaml +/docs/sync/readme +``` + +### With Hooks + +Auto-sync README when registries change: + +```yaml +# .claude/hooks.yaml +- event: on_file_save + pattern: "registry/*.json" + command: python skills/docs.sync.readme/readme_sync.py + blocking: false + description: Auto-sync README when registries change +``` + +### With plugin.sync + +Chain both sync operations: + +```bash +/plugin/sync && /docs/sync/readme +``` + +## Categorization Rules + +### Foundation Category + +**Criteria**: +- Skill name starts with: `skill.`, `registry.`, `workflow.` +- Core Betty framework functionality + +**Examples**: +- `skill.create`, `skill.define` +- `registry.update`, `registry.query` +- `workflow.compose`, `workflow.validate` + +### API Category + +**Criteria**: +- Skill name starts with: `api.` +- Tags include: `api`, `openapi`, `asyncapi` + +**Examples**: +- `api.define`, `api.validate` +- `api.generate-models`, `api.compatibility` + +### Infrastructure Category + +**Criteria**: +- Tags include: `agents`, `command`, `hook`, `policy`, `plugin`, `registry` +- Infrastructure and orchestration skills + +**Examples**: +- `agent.define`, `agent.run` +- `hook.define`, `hook.register` +- `plugin.sync`, `plugin.build` + +### Governance Category + +**Criteria**: +- Tags include: `governance`, `policy`, `audit` +- Policy enforcement and audit trails + +**Examples**: +- `policy.enforce` +- `audit.log` + +## Filtering Rules + +### ✅ Included + +- Skills with `status: active` +- Agents with `status: active` or `status: draft` +- Skills with meaningful descriptions + +### ❌ Excluded + +- Skills with `status: draft` +- Skills starting with `test.` +- Skills without names or descriptions + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "README.md not found" | Missing README file | Ensure README.md exists in repo root | +| "Registry file not found" | Missing registry | Run skill.define to populate registry | +| "Failed to parse JSON" | Invalid JSON | Fix JSON syntax in registry files | + +## Files Read + +- `README.md` – Current README content +- `registry/skills.json` – Skills registry +- `registry/agents.json` – Agents registry + +## Files Modified + +- `README.md` – Updated with current skills and agents +- `skills/docs.sync.readme/sync_report.json` – Sync statistics + +## Exit Codes + +- **0**: Success (README updated successfully) +- **1**: Failure (error during sync) + +## Logging + +Logs sync progress: + +``` +INFO: Starting README.md sync from registries... +INFO: Loading registry files... +INFO: Generating updated README content... +INFO: ✅ Updated README.md +INFO: - Foundation skills: 5 +INFO: - API skills: 4 +INFO: - Infrastructure skills: 9 +INFO: - Governance skills: 1 +INFO: - Total active skills: 19 +INFO: - Agents: 2 +``` + +## Best Practices + +1. **Run After Registry Changes**: Sync README whenever skills or agents are added/updated +2. **Include in CI/CD**: Add README sync to deployment pipelines +3. **Review Before Commit**: Check updated README before committing changes +4. **Use Hooks**: Set up auto-sync hooks for convenience +5. **Combine with plugin.sync**: Keep both plugin.yaml and README in sync +6. **Version Control**: Always commit README changes with skill/agent changes + +## Troubleshooting + +### README Not Updating + +**Problem**: Changes to registry don't appear in README + +**Solutions**: +- Ensure skills have `status: active` +- Check that skill names and descriptions are present +- Verify registry files are valid JSON +- Run `/skill/define` before syncing README + +### Skills in Wrong Category + +**Problem**: Skill appears in unexpected category + +**Solutions**: +- Check skill tags in skill.yaml +- Verify tag categorization rules above +- Add appropriate tags to skill.yaml +- Re-run skill.define to update registry + +### Section Markers Not Found + +**Problem**: "Section marker not found" warnings + +**Solutions**: +- Ensure README has expected section headers +- Check for typos in section headers +- Restore original README structure if modified +- Update section_marker strings in code if intentionally changed + +## Architecture + +### Skill Categories + +**Documentation** – docs.sync.readme maintains the README documentation layer by syncing registry state to the top-level README. + +### Design Principles + +- **Single Source of Truth**: Registries are the source of truth +- **Preserve Structure**: Only update specific sections +- **Maintain Style**: Keep original tone and formatting +- **Clear Categorization**: Logical grouping of skills by function +- **Idempotent**: Can be run multiple times safely + +## See Also + +- **plugin.sync** – Sync plugin.yaml with registries ([SKILL.md](../plugin.sync/SKILL.md)) +- **skill.define** – Validate and register skills ([SKILL.md](../skill.define/SKILL.md)) +- **agent.define** – Validate and register agents ([SKILL.md](../agent.define/SKILL.md)) +- **registry.update** – Update registries ([SKILL.md](../registry.update/SKILL.md)) +- **Betty Architecture** – Framework overview ([betty-architecture.md](../../docs/betty-architecture.md)) + +## Dependencies + +- **registry.update**: Registry management +- **betty.config**: Configuration constants and paths +- **betty.logging_utils**: Logging infrastructure + +## Status + +**Active** – Production-ready documentation skill + +## Version History + +- **0.1.0** (Oct 2025) – Initial implementation with skills categorization and agents documentation diff --git a/skills/docs.sync.readme/__init__.py b/skills/docs.sync.readme/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/docs.sync.readme/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/docs.sync.readme/readme_sync.py b/skills/docs.sync.readme/readme_sync.py new file mode 100755 index 0000000..7e57637 --- /dev/null +++ b/skills/docs.sync.readme/readme_sync.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 +""" +readme_sync.py - Implementation of the docs.sync.readme Skill +Regenerates the top-level README.md to reflect all current registered skills and agents. +""" + +import os +import sys +import json +import re +from typing import Dict, Any, List, Optional +from datetime import datetime, timezone +from pathlib import Path + + +from betty.config import BASE_DIR, REGISTRY_FILE, AGENTS_REGISTRY_FILE +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +def load_registry(registry_path: str) -> Dict[str, Any]: + """ + Load a JSON registry file. + + Args: + registry_path: Path to registry JSON file + + Returns: + Parsed registry data + """ + try: + with open(registry_path) as f: + return json.load(f) + except FileNotFoundError: + logger.warning(f"Registry file not found: {registry_path}") + return {"skills": []} if "skills" in registry_path else {"agents": []} + except json.JSONDecodeError as e: + logger.error(f"Failed to parse JSON from {registry_path}: {e}") + raise + + +def categorize_skills(skills: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]: + """ + Categorize skills by their tags into foundation, api, infrastructure, and governance groups. + + Args: + skills: List of skill dictionaries from registry + + Returns: + Dictionary mapping category names to lists of skills + """ + categories = { + "foundation": [], + "api": [], + "infrastructure": [], + "governance": [] + } + + for skill in skills: + # Only include active skills + if skill.get("status") != "active": + continue + + # Skip test skills + if skill.get("name", "").startswith("test."): + continue + + tags = skill.get("tags", []) + name = skill.get("name", "") + + # Categorize based on tags or name patterns + if any(tag in ["api", "openapi", "asyncapi"] for tag in tags) or name.startswith("api."): + categories["api"].append(skill) + elif any(tag in ["agents", "command", "hook", "policy", "plugin", "registry"] for tag in tags): + categories["infrastructure"].append(skill) + elif any(tag in ["governance", "policy", "audit"] for tag in tags): + categories["governance"].append(skill) + elif name.startswith("skill.") or name.startswith("registry.") or name.startswith("workflow."): + categories["foundation"].append(skill) + else: + # Default to infrastructure if unclear + categories["infrastructure"].append(skill) + + # Remove duplicates and sort by name + for category in categories: + seen = set() + unique_skills = [] + for skill in categories[category]: + if skill["name"] not in seen: + seen.add(skill["name"]) + unique_skills.append(skill) + categories[category] = sorted(unique_skills, key=lambda s: s["name"]) + + return categories + + +def format_skill_table(skills: List[Dict[str, Any]]) -> str: + """ + Format a list of skills as a markdown table. + + Args: + skills: List of skill dictionaries + + Returns: + Markdown table string + """ + if not skills: + return "| Skill | Purpose |\n|--------|----------|\n| _(No skills in this category)_ | |" + + lines = ["| Skill | Purpose |", "|--------|----------|"] + + for skill in skills: + name = skill.get("name", "") + # Get first line of description only + desc = skill.get("description", "").strip().split("\n")[0] + # Clean up description (remove extra whitespace) + desc = " ".join(desc.split()) + + lines.append(f"| **{name}** | {desc} |") + + return "\n".join(lines) + + +def format_agents_docs(agents: List[Dict[str, Any]]) -> str: + """ + Format agent documentation links. + + Args: + agents: List of agent dictionaries + + Returns: + Markdown list of agent links + """ + if not agents: + return "_(No agents registered)_" + + lines = [] + for agent in agents: + name = agent.get("name", "") + # Get first line of description only + desc = agent.get("description", "").strip().split("\n")[0] + desc = " ".join(desc.split()) + + lines.append(f"* [{name}](agents/{name}/README.md) — {desc}") + + return "\n".join(lines) + + +def update_readme_section( + content: str, + section_marker: str, + end_marker: str, + new_content: str +) -> str: + """ + Update a section of the README between two markers. + + Args: + content: Full README content + section_marker: Start marker (e.g., "## 🧩 Current Core Skills") + end_marker: End marker (e.g., "---") + new_content: New content to insert between markers + + Returns: + Updated README content + """ + # Find section start + section_start = content.find(section_marker) + if section_start == -1: + logger.warning(f"Section marker not found: {section_marker}") + return content + + # Find section end after the start - look for the end marker on its own line + search_start = section_start + len(section_marker) + end_marker_pattern = f"\n{end_marker}\n" + section_end = content.find(end_marker_pattern, search_start) + + if section_end == -1: + logger.warning(f"End marker not found after {section_marker}") + return content + + # Replace the section (include the newline before end marker) + before = content[:section_start] + after = content[section_end + 1:] # +1 to skip the first newline + + return before + section_marker + "\n\n" + new_content + "\n" + after + + +def generate_skills_section(categories: Dict[str, List[Dict[str, Any]]]) -> str: + """ + Generate the complete skills section content. + + Args: + categories: Dictionary of categorized skills + + Returns: + Markdown content for skills section + """ + lines = [ + "Betty's self-referential \"kernel\" of skills bootstraps the rest of the system:", + "" + ] + + # Foundation Skills + if categories["foundation"]: + lines.extend([ + "### Foundation Skills", + "", + format_skill_table(categories["foundation"]), + "" + ]) + + # API Development Skills + if categories["api"]: + lines.extend([ + "### API Development Skills", + "", + format_skill_table(categories["api"]), + "" + ]) + + # Infrastructure Skills + if categories["infrastructure"]: + lines.extend([ + "### Infrastructure Skills", + "", + format_skill_table(categories["infrastructure"]), + "" + ]) + + # Governance Skills (if any) + if categories["governance"]: + lines.extend([ + "### Governance Skills", + "", + format_skill_table(categories["governance"]), + "" + ]) + + lines.append("These skills form the baseline for an **AI-native SDLC** where creation, validation, registration, and orchestration are themselves skills.") + + return "\n".join(lines) + + +def update_agents_section(content: str, agents: List[Dict[str, Any]]) -> str: + """ + Update the Agents Documentation section. + + Args: + content: Full README content + agents: List of active agents + + Returns: + Updated README content + """ + agents_docs = format_agents_docs(agents) + + # Find the "### Agents Documentation" section + section_start = content.find("### Agents Documentation") + if section_start == -1: + logger.warning("Agents Documentation section not found") + return content + + # Find the next ### or ## to determine section end + next_section = content.find("\n##", section_start + 25) + if next_section == -1: + next_section = len(content) + + # Find "Each agent has a" line as the start of actual content + intro_start = content.find("Each agent has a `README.md` in its directory:", section_start) + if intro_start == -1: + intro_start = section_start + 25 + else: + intro_start += len("Each agent has a `README.md` in its directory:") + + before = content[:intro_start] + after = content[next_section:] + + return before + "\n" + agents_docs + "\n\n" + after + + +def generate_readme( + skills_data: Dict[str, Any], + agents_data: Dict[str, Any] +) -> tuple[str, Dict[str, Any]]: + """ + Generate updated README.md content. + + Args: + skills_data: Parsed skills.json + agents_data: Parsed agents.json + + Returns: + Tuple of (updated_readme_content, report_dict) + """ + readme_path = os.path.join(BASE_DIR, "README.md") + + # Read current README + try: + with open(readme_path) as f: + content = f.read() + except FileNotFoundError: + logger.error(f"README.md not found at {readme_path}") + raise + + # Categorize skills + skills = skills_data.get("skills", []) + categories = categorize_skills(skills) + + # Get active agents + agents = [a for a in agents_data.get("agents", []) if a.get("status") == "active" or a.get("status") == "draft"] + agents = sorted(agents, key=lambda a: a["name"]) + + # Generate new skills section + skills_section = generate_skills_section(categories) + + # Update skills section + content = update_readme_section( + content, + "## 🧩 Current Core Skills", + "---", + skills_section + ) + + # Update agents section + content = update_agents_section(content, agents) + + # Generate report + report = { + "skills_by_category": { + "foundation": len(categories["foundation"]), + "api": len(categories["api"]), + "infrastructure": len(categories["infrastructure"]), + "governance": len(categories["governance"]) + }, + "total_skills": sum(len(skills) for skills in categories.values()), + "agents_count": len(agents), + "timestamp": datetime.now(timezone.utc).isoformat() + } + + return content, report + + +def main(): + """Main CLI entry point.""" + logger.info("Starting README.md sync from registries...") + + try: + # Load registries + logger.info("Loading registry files...") + skills_data = load_registry(REGISTRY_FILE) + agents_data = load_registry(AGENTS_REGISTRY_FILE) + + # Generate updated README + logger.info("Generating updated README content...") + readme_content, report = generate_readme(skills_data, agents_data) + + # Write README + readme_path = os.path.join(BASE_DIR, "README.md") + with open(readme_path, 'w') as f: + f.write(readme_content) + + logger.info(f"✅ Updated README.md") + logger.info(f" - Foundation skills: {report['skills_by_category']['foundation']}") + logger.info(f" - API skills: {report['skills_by_category']['api']}") + logger.info(f" - Infrastructure skills: {report['skills_by_category']['infrastructure']}") + logger.info(f" - Governance skills: {report['skills_by_category']['governance']}") + logger.info(f" - Total active skills: {report['total_skills']}") + logger.info(f" - Agents: {report['agents_count']}") + + # Write report + report_path = os.path.join(BASE_DIR, "skills", "docs.sync.readme", "sync_report.json") + with open(report_path, 'w') as f: + json.dump(report, f, indent=2) + + result = { + "ok": True, + "status": "success", + "readme_path": readme_path, + "report": report + } + + print(json.dumps(result, indent=2)) + sys.exit(0) + + except Exception as e: + logger.error(f"Failed to sync README: {e}") + result = { + "ok": False, + "status": "failed", + "error": str(e) + } + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/docs.sync.readme/skill.yaml b/skills/docs.sync.readme/skill.yaml new file mode 100644 index 0000000..991c202 --- /dev/null +++ b/skills/docs.sync.readme/skill.yaml @@ -0,0 +1,31 @@ +name: docs.sync.readme +version: 0.1.0 +description: > + Regenerate the top-level README.md to reflect all current registered skills and agents. + Pulls from registry/skills.json and registry/agents.json, groups by category, and updates + documentation sections while maintaining repo style and tone. +inputs: [] +outputs: + - README.md + - sync_report.json +dependencies: + - registry.update +status: active + +entrypoints: + - command: /docs/sync/readme + handler: readme_sync.py + runtime: python + description: > + Sync README.md with current registry state. Updates skills tables, agents links, + and documentation map to reflect all registered skills and agents. + parameters: [] + permissions: + - filesystem:read + - filesystem:write + +tags: + - documentation + - registry + - automation + - maintenance diff --git a/skills/docs.sync.readme/sync_report.json b/skills/docs.sync.readme/sync_report.json new file mode 100644 index 0000000..ea0a8ab --- /dev/null +++ b/skills/docs.sync.readme/sync_report.json @@ -0,0 +1,11 @@ +{ + "skills_by_category": { + "foundation": 5, + "api": 4, + "infrastructure": 11, + "governance": 0 + }, + "total_skills": 20, + "agents_count": 2, + "timestamp": "2025-10-23T19:58:22.843605+00:00" +} diff --git a/skills/docs.validate.skilldocs/SKILL.md b/skills/docs.validate.skilldocs/SKILL.md new file mode 100644 index 0000000..bd4a82b --- /dev/null +++ b/skills/docs.validate.skilldocs/SKILL.md @@ -0,0 +1,335 @@ +# docs.validate.skill_docs + +## Overview + +The `docs.validate.skill_docs` skill validates SKILL.md documentation files against their corresponding skill.yaml manifests to ensure completeness, consistency, and quality. This skill helps maintain high documentation standards across all Betty Framework skills by automatically checking for required sections and verifying that documented fields match the manifest. + +## Purpose + +Documentation quality is critical for skill discoverability and usability. This skill addresses the common problem of documentation drift, where SKILL.md files become outdated or incomplete as skills evolve. By automatically validating documentation, this skill ensures that: + +- All required documentation sections are present +- Documented inputs and outputs match the manifest +- Skill metadata is consistent between files +- Documentation follows Betty Framework standards + +This enables developers to maintain high-quality, trustworthy documentation with minimal manual effort. + +## Usage + +### Basic Usage + +```bash +python skills/docs.validate.skill_docs/skill_docs_validate.py [options] +``` + +### Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `skill_path` | string | Yes | - | Path to skill directory containing skill.yaml and SKILL.md | +| `--summary` | boolean | No | false | Print a short summary table instead of full JSON output | +| `--no-headers` | boolean | No | false | Skip validation of required headers | +| `--no-manifest-parity` | Skip validation of manifest field parity | + +### Options + +- `--summary`: Displays a concise table showing validation status for each skill +- `--no-headers`: Skips checking for required section headers +- `--no-manifest-parity`: Skips checking that SKILL.md matches skill.yaml fields + +## Inputs + +The skill accepts the following inputs: + +- **skill_path** (required): Path to the skill directory to validate. Can be a single skill directory or a parent directory containing multiple skills for batch validation. + +- **summary** (optional, default: false): When enabled, outputs a formatted table summary instead of detailed JSON output. Useful for quick validation checks. + +- **check_headers** (optional, default: true): Validates that SKILL.md contains all required section headers: Overview, Purpose, Usage, Inputs, Outputs, Examples, Integration, and Errors. + +- **check_manifest_parity** (optional, default: true): Validates that documented inputs, outputs, and metadata match the skill.yaml manifest. + +## Outputs + +The skill produces a JSON validation report with the following fields: + +- **ok** (boolean): Overall validation status (true if valid, false if errors found) +- **status** (string): "valid" or "invalid" +- **skill_path** (string): Path to the validated skill +- **skill_name** (string): Name of the skill from manifest +- **valid** (boolean): Whether the skill documentation is valid +- **errors** (array): List of validation errors +- **warnings** (array): List of validation warnings +- **validation_report** (object): Detailed validation results + +Each error/warning includes: +- `type`: Error type (e.g., "missing_header", "undocumented_input") +- `message`: Human-readable description +- `severity`: "error" or "warning" +- `file`: Affected file (if applicable) +- `suggestion`: Recommended fix (if applicable) + +## Examples + +### Example 1: Validate a Single Skill + +```bash +python skills/docs.validate.skill_docs/skill_docs_validate.py skills/api.validate +``` + +**Output**: +```json +{ + "ok": true, + "status": "valid", + "skill_path": "skills/api.validate", + "errors": [], + "warnings": [], + "details": { + "valid": true, + "skill_name": "api.validate", + "skill_path": "skills/api.validate", + "errors": [], + "warnings": [], + "checks_run": { + "headers": true, + "manifest_parity": true + } + } +} +``` + +### Example 2: Batch Validate All Skills with Summary + +```bash +python skills/docs.validate.skill_docs/skill_docs_validate.py skills --summary +``` + +**Output**: +``` +================================================================================ +SKILL DOCUMENTATION VALIDATION SUMMARY +================================================================================ +Skill Name Status Errors Warnings +-------------------------------------------------------------------------------- +api.validate VALID 0 0 +docs.validate.skill_docs VALID 0 0 +generate.docs VALID 0 1 +workflow.validate INVALID 2 0 +-------------------------------------------------------------------------------- +Total: 4 skills | Valid: 3 | Total Errors: 2 | Total Warnings: 1 +================================================================================ +``` + +### Example 3: Validate with Detailed Error Report + +```bash +python skills/docs.validate.skill_docs/skill_docs_validate.py skills/workflow.validate +``` + +**Output**: +```json +{ + "ok": false, + "status": "invalid", + "skill_path": "skills/workflow.validate", + "errors": [ + { + "type": "missing_header", + "message": "Required header 'Integration' not found in SKILL.md", + "severity": "error", + "file": "SKILL.md", + "suggestion": "Add a '## Integration' section to SKILL.md" + }, + { + "type": "undocumented_input", + "message": "Input 'strict_mode' from manifest not documented in SKILL.md", + "severity": "error", + "file": "SKILL.md", + "suggestion": "Document the 'strict_mode' input in the Inputs section" + } + ], + "warnings": [], + "details": { + "valid": false, + "skill_name": "workflow.validate", + "checks_run": { + "headers": true, + "manifest_parity": true + } + } +} +``` + +### Example 4: Skip Header Validation + +```bash +python skills/docs.validate.skill_docs/skill_docs_validate.py skills/api.validate --no-headers +``` + +This validates only manifest field parity, skipping header checks. + +## Validation Rules + +### Required Headers + +The following headers must be present in SKILL.md: + +1. **Overview**: Brief description of the skill +2. **Purpose**: Detailed explanation of the problem it solves +3. **Usage**: How to use the skill with command examples +4. **Inputs**: Documentation of all input parameters +5. **Outputs**: Documentation of all output fields +6. **Examples**: Practical usage examples +7. **Integration**: How to integrate with workflows or hooks +8. **Errors**: Common errors and exit codes + +Alternative header variations are accepted (e.g., "How to Use" for "Usage", "Parameters" for "Inputs"). + +### Manifest Parity Checks + +The skill validates that: + +- **Skill name** from manifest appears in the documentation +- **All inputs** defined in skill.yaml are documented in SKILL.md +- **All outputs** defined in skill.yaml are documented in SKILL.md +- **Status** field uses standard values (active, deprecated, experimental) +- **Tags** are defined for skill discoverability + +### Error Types + +| Error Type | Severity | Description | +|------------|----------|-------------| +| `missing_file` | error | SKILL.md file not found | +| `missing_header` | error | Required section header missing | +| `undocumented_input` | error | Manifest input not documented | +| `undocumented_output` | warning | Manifest output not documented | +| `missing_skill_name` | warning | Skill name not mentioned in docs | +| `invalid_status` | warning | Non-standard status value | +| `missing_tags` | warning | No tags defined in manifest | + +## Integration + +### Integration with Hooks + +You can integrate this skill with Betty's hook system to automatically validate documentation before commits: + +```yaml +# .betty/hooks/pre-commit.yaml +name: validate-docs-pre-commit +event: pre-commit +skill: docs.validate.skill_docs +inputs: + skill_path: ${CHANGED_SKILL_DIR} + summary: true +on_failure: block +``` + +### Use in Workflows + +Example workflow for continuous documentation validation: + +```yaml +name: validate-all-skill-docs +description: Validate all skill documentation files + +steps: + - name: validate-docs + skill: docs.validate.skill_docs + inputs: + skill_path: skills + summary: true + check_headers: true + check_manifest_parity: true + + - name: report-results + condition: ${{ steps.validate-docs.outputs.valid == false }} + action: fail + message: "Documentation validation failed. See errors above." +``` + +### CI/CD Integration + +Add to your CI/CD pipeline: + +```bash +# Validate all skills and fail on errors +python skills/docs.validate.skill_docs/skill_docs_validate.py skills --summary || exit 1 +``` + +## Errors + +### Exit Codes + +| Code | Description | +|------|-------------| +| 0 | All validations passed successfully | +| 1 | Validation failed (errors found) or usage error | + +### Common Validation Errors + +#### Missing Header Error + +**Error**: +```json +{ + "type": "missing_header", + "message": "Required header 'Integration' not found in SKILL.md" +} +``` + +**Fix**: Add the missing section to SKILL.md: +```markdown +## Integration + +Describe how to integrate this skill with workflows and hooks. +``` + +#### Undocumented Input Error + +**Error**: +```json +{ + "type": "undocumented_input", + "message": "Input 'api_key' from manifest not documented in SKILL.md" +} +``` + +**Fix**: Document the input in the Inputs section: +```markdown +## Inputs + +- **api_key** (optional): API authentication key for external service +``` + +#### Invalid Status Warning + +**Error**: +```json +{ + "type": "invalid_status", + "message": "Manifest status 'beta' is not a standard value" +} +``` + +**Fix**: Update skill.yaml to use a standard status: +```yaml +status: experimental # Use: active, deprecated, or experimental +``` + +## Dependencies + +- **Python**: 3.8+ +- **PyYAML**: For parsing skill.yaml manifests +- **Betty Framework**: context.schema dependency + +## See Also + +- [generate.docs](../generate.docs/SKILL.md) - Generate SKILL.md from manifests +- [workflow.validate](../workflow.validate/SKILL.md) - Validate workflow definitions +- [api.validate](../api.validate/SKILL.md) - Validate API specifications + +## Version + +v0.1.0 diff --git a/skills/docs.validate.skilldocs/__init__.py b/skills/docs.validate.skilldocs/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/docs.validate.skilldocs/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/docs.validate.skilldocs/skill.yaml b/skills/docs.validate.skilldocs/skill.yaml new file mode 100644 index 0000000..454ca68 --- /dev/null +++ b/skills/docs.validate.skilldocs/skill.yaml @@ -0,0 +1,57 @@ +name: docs.validate.skilldocs +version: 0.1.0 +description: Validate SKILL.md documentation files against their skill.yaml manifests to ensure completeness and consistency + +inputs: + - name: skill_path + type: string + required: true + description: Path to skill directory containing skill.yaml and SKILL.md + + - name: summary + type: boolean + required: false + default: false + description: Print a short summary table of validation results instead of full JSON output + + - name: check_headers + type: boolean + required: false + default: true + description: Validate that SKILL.md contains required section headers + + - name: check_manifest_parity + type: boolean + required: false + default: true + description: Validate that SKILL.md documentation matches skill.yaml manifest fields + +outputs: + - name: valid + type: boolean + description: Whether the skill documentation is valid + + - name: validation_report + type: object + description: Detailed validation results including errors and warnings + + - name: errors + type: array + description: List of validation errors found + + - name: warnings + type: array + description: List of validation warnings found + +dependencies: + - context.schema + +entrypoints: + - command: /docs/validate/skill-docs + handler: skill_docs_validate.py + runtime: python + permissions: + - filesystem:read + +status: active +tags: [documentation, validation, quality-assurance, skill-management] diff --git a/skills/docs.validate.skilldocs/skill_docs_validate.py b/skills/docs.validate.skilldocs/skill_docs_validate.py new file mode 100755 index 0000000..b72db7f --- /dev/null +++ b/skills/docs.validate.skilldocs/skill_docs_validate.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python3 +""" +Validate SKILL.md documentation against skill.yaml manifests. + +This skill ensures that SKILL.md files contain all required sections and that +documented fields match the corresponding skill.yaml manifest. +""" + +import json +import sys +import os +import re +from pathlib import Path +from typing import Any, Dict, List, Optional, Set + +import yaml + + +from betty.errors import BettyError, SkillValidationError +from betty.logging_utils import setup_logger +from betty.validation import validate_path, ValidationError +from betty.telemetry_integration import telemetry_tracked + +logger = setup_logger(__name__) + +# Required headers in SKILL.md +REQUIRED_HEADERS = [ + "Overview", + "Purpose", + "Usage", + "Inputs", + "Outputs", + "Examples", + "Integration", + "Errors" +] + +# Alternative header variations that are acceptable +HEADER_VARIATIONS = { + "Overview": ["Overview", "## Overview", "Description"], + "Purpose": ["Purpose", "## Purpose"], + "Usage": ["Usage", "## Usage", "How to Use", "## How to Use"], + "Inputs": ["Inputs", "## Inputs", "Parameters", "## Parameters"], + "Outputs": ["Outputs", "## Outputs", "Output", "## Output"], + "Examples": ["Examples", "## Examples", "Example", "## Example"], + "Integration": ["Integration", "## Integration", "Integration with Hooks", "## Integration with Hooks", "Use in Workflows", "## Use in Workflows"], + "Errors": ["Errors", "## Errors", "Error Codes", "## Error Codes", "Exit Codes", "## Exit Codes", "Common Errors", "## Common Errors"] +} + + +class ValidationIssue: + """Represents a validation issue (error or warning).""" + + def __init__(self, issue_type: str, message: str, severity: str = "error", + file_path: Optional[str] = None, suggestion: Optional[str] = None): + self.issue_type = issue_type + self.message = message + self.severity = severity + self.file_path = file_path + self.suggestion = suggestion + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + result = { + "type": self.issue_type, + "message": self.message, + "severity": self.severity + } + if self.file_path: + result["file"] = self.file_path + if self.suggestion: + result["suggestion"] = self.suggestion + return result + + +class SkillDocsValidator: + """Validates SKILL.md documentation against skill.yaml manifest.""" + + def __init__(self, skill_path: str, check_headers: bool = True, + check_manifest_parity: bool = True): + self.skill_path = Path(skill_path) + self.check_headers = check_headers + self.check_manifest_parity = check_manifest_parity + self.errors: List[ValidationIssue] = [] + self.warnings: List[ValidationIssue] = [] + self.manifest: Optional[Dict[str, Any]] = None + self.skill_md_content: Optional[str] = None + + def validate(self) -> Dict[str, Any]: + """Run all validation checks.""" + try: + # Load manifest and documentation + self._load_files() + + # Run checks + if self.check_headers: + self._validate_headers() + + if self.check_manifest_parity: + self._validate_manifest_parity() + + # Build response + return { + "valid": len(self.errors) == 0, + "skill_name": self.manifest.get("name") if self.manifest else "unknown", + "skill_path": str(self.skill_path), + "errors": [e.to_dict() for e in self.errors], + "warnings": [w.to_dict() for w in self.warnings], + "checks_run": { + "headers": self.check_headers, + "manifest_parity": self.check_manifest_parity + } + } + + except Exception as exc: + logger.error("Validation failed: %s", exc) + self.errors.append(ValidationIssue( + "validation_error", + str(exc), + severity="error" + )) + return { + "valid": False, + "skill_name": "unknown", + "skill_path": str(self.skill_path), + "errors": [e.to_dict() for e in self.errors], + "warnings": [w.to_dict() for w in self.warnings], + "checks_run": { + "headers": self.check_headers, + "manifest_parity": self.check_manifest_parity + } + } + + def _load_files(self) -> None: + """Load skill.yaml and SKILL.md files.""" + # Validate skill path + try: + validate_path(str(self.skill_path), must_exist=True) + except ValidationError as exc: + raise SkillValidationError(f"Invalid skill path: {exc}") from exc + + if not self.skill_path.is_dir(): + raise SkillValidationError(f"Skill path is not a directory: {self.skill_path}") + + # Load skill.yaml + manifest_path = self.skill_path / "skill.yaml" + if not manifest_path.exists(): + raise SkillValidationError(f"skill.yaml not found at {manifest_path}") + + try: + with open(manifest_path, 'r', encoding='utf-8') as f: + self.manifest = yaml.safe_load(f) + except Exception as exc: + raise SkillValidationError(f"Failed to parse skill.yaml: {exc}") from exc + + # Load SKILL.md + skill_md_path = self.skill_path / "SKILL.md" + if not skill_md_path.exists(): + self.errors.append(ValidationIssue( + "missing_file", + "SKILL.md file not found", + severity="error", + file_path=str(skill_md_path), + suggestion="Create SKILL.md documentation file" + )) + self.skill_md_content = "" + return + + try: + with open(skill_md_path, 'r', encoding='utf-8') as f: + self.skill_md_content = f.read() + except Exception as exc: + raise SkillValidationError(f"Failed to read SKILL.md: {exc}") from exc + + def _validate_headers(self) -> None: + """Validate that SKILL.md contains all required headers.""" + if not self.skill_md_content: + return + + # Extract all headers from the markdown + header_pattern = re.compile(r'^#{1,6}\s+(.+)$', re.MULTILINE) + found_headers = set() + + for match in header_pattern.finditer(self.skill_md_content): + header_text = match.group(1).strip() + found_headers.add(header_text) + + # Check each required header + for required_header in REQUIRED_HEADERS: + variations = HEADER_VARIATIONS.get(required_header, [required_header]) + + # Check if any variation exists + found = False + for variation in variations: + # Remove markdown prefix for comparison + clean_variation = variation.replace("#", "").strip() + if any(clean_variation.lower() in h.lower() for h in found_headers): + found = True + break + + if not found: + self.errors.append(ValidationIssue( + "missing_header", + f"Required header '{required_header}' not found in SKILL.md", + severity="error", + file_path="SKILL.md", + suggestion=f"Add a '## {required_header}' section to SKILL.md" + )) + + def _validate_manifest_parity(self) -> None: + """Validate that SKILL.md matches skill.yaml manifest fields.""" + if not self.manifest or not self.skill_md_content: + return + + # Check skill name + skill_name = self.manifest.get("name", "") + if skill_name and skill_name not in self.skill_md_content: + self.warnings.append(ValidationIssue( + "missing_skill_name", + f"Skill name '{skill_name}' not found in SKILL.md", + severity="warning", + suggestion=f"Include the skill name '{skill_name}' in the documentation" + )) + + # Check inputs + manifest_inputs = self.manifest.get("inputs", []) + if manifest_inputs: + self._validate_inputs_documented(manifest_inputs) + + # Check outputs + manifest_outputs = self.manifest.get("outputs", []) + if manifest_outputs: + self._validate_outputs_documented(manifest_outputs) + + # Check status + status = self.manifest.get("status", "") + if status and status not in ["active", "deprecated", "experimental"]: + self.warnings.append(ValidationIssue( + "invalid_status", + f"Manifest status '{status}' is not a standard value", + severity="warning", + suggestion="Use 'active', 'deprecated', or 'experimental'" + )) + + # Check tags + tags = self.manifest.get("tags", []) + if not tags: + self.warnings.append(ValidationIssue( + "missing_tags", + "No tags defined in skill.yaml manifest", + severity="warning", + suggestion="Add relevant tags to improve skill discoverability" + )) + + def _validate_inputs_documented(self, inputs: List[Any]) -> None: + """Validate that all manifest inputs are documented in SKILL.md.""" + for input_spec in inputs: + # Handle both string format and dict format + if isinstance(input_spec, str): + # Simple string format: "input_name (optional)" + input_name = input_spec.split("(")[0].strip() + elif isinstance(input_spec, dict): + # Dictionary format with name, type, description + input_name = input_spec.get("name", "") + else: + logger.warning("Unexpected input format: %s", type(input_spec)) + continue + + if not input_name: + continue + + # Check if input is mentioned in the documentation + # Look for various patterns: input_name, --input-name, `input_name`, etc. + patterns = [ + input_name, + f"`{input_name}`", + f"--{input_name.replace('_', '-')}", + f"`--{input_name.replace('_', '-')}`" + ] + + found = any(pattern in self.skill_md_content for pattern in patterns) + + if not found: + self.errors.append(ValidationIssue( + "undocumented_input", + f"Input '{input_name}' from manifest not documented in SKILL.md", + severity="error", + file_path="SKILL.md", + suggestion=f"Document the '{input_name}' input in the Inputs section" + )) + + def _validate_outputs_documented(self, outputs: List[Any]) -> None: + """Validate that all manifest outputs are documented in SKILL.md.""" + for output_spec in outputs: + # Handle both string format and dict format + if isinstance(output_spec, str): + # Simple string format: "output_name.json" or just "output_name" + output_name = output_spec.split(".")[0].strip() + elif isinstance(output_spec, dict): + # Dictionary format with name, type, description + output_name = output_spec.get("name", "") + else: + logger.warning("Unexpected output format: %s", type(output_spec)) + continue + + if not output_name: + continue + + # Check if output is mentioned in the documentation + patterns = [ + output_name, + f"`{output_name}`", + f'"{output_name}"' + ] + + found = any(pattern in self.skill_md_content for pattern in patterns) + + if not found: + self.warnings.append(ValidationIssue( + "undocumented_output", + f"Output '{output_name}' from manifest not documented in SKILL.md", + severity="warning", + file_path="SKILL.md", + suggestion=f"Document the '{output_name}' output in the Outputs section" + )) + + +def build_response(ok: bool, skill_path: str, errors: Optional[List[Dict[str, Any]]] = None, + warnings: Optional[List[Dict[str, Any]]] = None, + details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Build standard response format.""" + return { + "ok": ok, + "status": "valid" if ok else "invalid", + "skill_path": skill_path, + "errors": errors or [], + "warnings": warnings or [], + "details": details or {} + } + + +def print_summary_table(results: List[Dict[str, Any]]) -> None: + """Print a summary table of validation results.""" + print("\n" + "="*80) + print("SKILL DOCUMENTATION VALIDATION SUMMARY") + print("="*80) + + if not results: + print("No skills validated.") + return + + # Calculate column widths + max_name_len = max(len(r.get("skill_name", "")) for r in results) + max_name_len = max(max_name_len, len("Skill Name")) + + # Print header + print(f"{'Skill Name':<{max_name_len}} {'Status':<8} {'Errors':<8} {'Warnings':<8}") + print("-" * 80) + + # Print each result + total_errors = 0 + total_warnings = 0 + valid_count = 0 + + for result in results: + skill_name = result.get("skill_name", "unknown") + valid = result.get("valid", False) + error_count = len(result.get("errors", [])) + warning_count = len(result.get("warnings", [])) + + status = "VALID" if valid else "INVALID" + status_color = status + + print(f"{skill_name:<{max_name_len}} {status:<8} {error_count:<8} {warning_count:<8}") + + total_errors += error_count + total_warnings += warning_count + if valid: + valid_count += 1 + + # Print summary + print("-" * 80) + print(f"Total: {len(results)} skills | Valid: {valid_count} | " + f"Total Errors: {total_errors} | Total Warnings: {total_warnings}") + print("="*80 + "\n") + + +@telemetry_tracked(skill_name="docs.validate.skill_docs", caller="cli") +def main(argv: Optional[List[str]] = None) -> int: + """Entry point for CLI execution.""" + argv = argv or sys.argv[1:] + + # Parse arguments + if len(argv) == 0: + response = build_response( + False, + "", + errors=[{ + "type": "usage_error", + "message": "Usage: skill_docs_validate.py [--summary] [--no-headers] [--no-manifest-parity]", + "severity": "error" + }] + ) + print(json.dumps(response, indent=2)) + return 1 + + skill_path = argv[0] + summary_mode = "--summary" in argv + check_headers = "--no-headers" not in argv + check_manifest_parity = "--no-manifest-parity" not in argv + + try: + # Handle batch validation of multiple skills if path is parent directory + skill_dir = Path(skill_path) + + # Check if this is a single skill or batch validation + if (skill_dir / "skill.yaml").exists(): + # Single skill validation + validator = SkillDocsValidator( + skill_path, + check_headers=check_headers, + check_manifest_parity=check_manifest_parity + ) + result = validator.validate() + + if summary_mode: + print_summary_table([result]) + else: + response = build_response( + result["valid"], + skill_path, + errors=result["errors"], + warnings=result["warnings"], + details=result + ) + print(json.dumps(response, indent=2)) + + return 0 if result["valid"] else 1 + + else: + # Batch validation - check if directory contains skill subdirectories + if not skill_dir.is_dir(): + response = build_response( + False, + skill_path, + errors=[{ + "type": "invalid_path", + "message": f"Path is not a directory: {skill_path}", + "severity": "error" + }] + ) + print(json.dumps(response, indent=2)) + return 1 + + # Find all skill directories + results = [] + for subdir in sorted(skill_dir.iterdir()): + if subdir.is_dir() and (subdir / "skill.yaml").exists(): + validator = SkillDocsValidator( + str(subdir), + check_headers=check_headers, + check_manifest_parity=check_manifest_parity + ) + result = validator.validate() + results.append(result) + + if not results: + response = build_response( + False, + skill_path, + errors=[{ + "type": "no_skills_found", + "message": f"No skills found in directory: {skill_path}", + "severity": "error" + }] + ) + print(json.dumps(response, indent=2)) + return 1 + + # Output results + if summary_mode: + print_summary_table(results) + else: + # Print full JSON for each skill + for result in results: + response = build_response( + result["valid"], + result["skill_path"], + errors=result["errors"], + warnings=result["warnings"], + details=result + ) + print(json.dumps(response, indent=2)) + print() # Blank line between results + + # Return error if any skill is invalid + any_invalid = any(not r["valid"] for r in results) + return 1 if any_invalid else 0 + + except Exception as exc: + logger.error("Validation failed: %s", exc, exc_info=True) + response = build_response( + False, + skill_path, + errors=[{ + "type": "validation_error", + "message": str(exc), + "severity": "error" + }] + ) + print(json.dumps(response, indent=2)) + return 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/skills/epic.decompose/SKILL.md b/skills/epic.decompose/SKILL.md new file mode 100644 index 0000000..3a09631 --- /dev/null +++ b/skills/epic.decompose/SKILL.md @@ -0,0 +1,86 @@ +# epic.decompose + +Take an Epic (as Markdown) and decompose it into user stories. Analyzes Epic document and identifies major deliverables, grouping them by persona or capability. + +## Overview + +**Purpose:** Take an Epic (as Markdown) and decompose it into user stories. Analyzes Epic document and identifies major deliverables, grouping them by persona or capability. + +**Command:** `/epic/decompose` + +## Usage + +### Basic Usage + +```bash +python3 skills/epic/decompose/epic_decompose.py +``` + +### With Arguments + +```bash +python3 skills/epic/decompose/epic_decompose.py \ + --epic_file_(string,_required):_path_to_the_epic.md_file_to_decompose "value" \ + --max_stories_(integer,_optional):_maximum_number_of_stories_to_generate_(default:_5) "value" \ + --output_path_(string,_optional):_where_to_save_the_stories.json_file_(default:_./stories.json) "value" \ + --output-format json +``` + +## Inputs + +- **epic_file (string, required): Path to the epic.md file to decompose** +- **max_stories (integer, optional): Maximum number of stories to generate (default: 5)** +- **output_path (string, optional): Where to save the stories.json file (default: ./stories.json)** + +## Outputs + +- **stories.json: Structured JSON list of story summaries with persona, goal, benefit, and acceptance criteria** + +## Artifact Metadata + +### Consumes + +- `agile-epic` + +### Produces + +- `user-stories-list` + +## Examples + +- python3 skills/epic.decompose/epic_decompose.py --epic-file ./epic.md --max-stories 5 + +## Permissions + +- `filesystem:read` +- `filesystem:write` + +## Implementation Notes + +Parse Markdown structure to extract Epic components. Use NLP techniques to identify distinct user stories. Ensure stories are independent and testable (INVEST criteria). Generate meaningful acceptance criteria. Validate output against JSON schema. Include metadata for traceability to source Epic. + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - epic.decompose +``` + +## Testing + +Run tests with: + +```bash +pytest skills/epic/decompose/test_epic_decompose.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/epic.decompose/__init__.py b/skills/epic.decompose/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/epic.decompose/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/epic.decompose/epic_decompose.py b/skills/epic.decompose/epic_decompose.py new file mode 100755 index 0000000..7db229e --- /dev/null +++ b/skills/epic.decompose/epic_decompose.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 +""" +epic.decompose - Take an Epic (as Markdown) and decompose it into user stories. Analyzes Epic document and identifies major deliverables, grouping them by persona or capability. + +Generated by meta.skill with Betty Framework certification +""" + +import os +import sys +import json +import yaml +import re +from pathlib import Path +from typing import Dict, List, Any, Optional + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.certification import certified_skill + +logger = setup_logger(__name__) + + +class EpicDecompose: + """ + Take an Epic (as Markdown) and decompose it into user stories. Analyzes Epic document and identifies major deliverables, grouping them by persona or capability. + """ + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + @certified_skill("epic.decompose") + def execute(self, epic_file: Optional[str] = None, max_stories: Optional[int] = None, + output_path: Optional[str] = None) -> Dict[str, Any]: + """ + Execute the skill + + Args: + epic_file: Path to the epic.md file to decompose + max_stories: Maximum number of stories to generate (default: 5) + output_path: Where to save the stories.json file (default: ./stories.json) + + Returns: + Dict with execution results + """ + try: + logger.info("Executing epic.decompose...") + + # Validate required inputs + if not epic_file: + raise ValueError("epic_file is required") + + # Set defaults + if max_stories is None: + max_stories = 5 + if not output_path: + output_path = "./stories.json" + + # Read Epic file + epic_path = Path(epic_file) + if not epic_path.exists(): + raise FileNotFoundError(f"Epic file not found: {epic_file}") + + epic_content = epic_path.read_text() + + # Parse Epic and extract information + epic_data = self._parse_epic(epic_content) + + # Generate user stories + stories = self._generate_stories(epic_data, max_stories) + + # Write stories to JSON file + output_file = Path(output_path) + output_file.parent.mkdir(parents=True, exist_ok=True) + + with open(output_file, 'w') as f: + json.dump(stories, f, indent=2) + + logger.info(f"Generated {len(stories)} stories and saved to {output_path}") + + result = { + "ok": True, + "status": "success", + "message": f"Decomposed Epic into {len(stories)} user stories", + "output_file": str(output_path), + "story_count": len(stories), + "artifact_type": "user-stories-list", + "next_step": "Use story.write to generate formatted story documents" + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + def _parse_epic(self, content: str) -> Dict[str, Any]: + """ + Parse Epic markdown to extract components + + Args: + content: Epic markdown content + + Returns: + Dict with parsed Epic data + """ + epic_data = { + "title": "", + "summary": "", + "background": "", + "acceptance_criteria": [], + "stakeholders": [] + } + + # Extract title + title_match = re.search(r'^# Epic: (.+)$', content, re.MULTILINE) + if title_match: + epic_data["title"] = title_match.group(1).strip() + + # Extract summary + summary_match = re.search(r'## Summary\s+(.+?)(?=##|$)', content, re.DOTALL) + if summary_match: + epic_data["summary"] = summary_match.group(1).strip() + + # Extract background + background_match = re.search(r'## Background\s+(.+?)(?=##|$)', content, re.DOTALL) + if background_match: + epic_data["background"] = background_match.group(1).strip() + + # Extract acceptance criteria + ac_match = re.search(r'## Acceptance Criteria\s+(.+?)(?=##|$)', content, re.DOTALL) + if ac_match: + ac_text = ac_match.group(1).strip() + # Extract checkbox items + criteria = re.findall(r'- \[ \] (.+)', ac_text) + epic_data["acceptance_criteria"] = criteria + + # Extract stakeholders + stakeholders_match = re.search(r'## Stakeholders\s+(.+?)(?=##|$)', content, re.DOTALL) + if stakeholders_match: + sh_text = stakeholders_match.group(1).strip() + # Extract bullet points + stakeholders = re.findall(r'- \*\*(.+?)\*\*', sh_text) + epic_data["stakeholders"] = stakeholders + + return epic_data + + def _generate_stories(self, epic_data: Dict[str, Any], max_stories: int) -> List[Dict[str, Any]]: + """ + Generate user stories from Epic data + + Args: + epic_data: Parsed Epic data + max_stories: Maximum number of stories to generate + + Returns: + List of user story objects + """ + stories = [] + + # Generate stories based on acceptance criteria and epic content + # This is a simplified approach - in a real system, you might use NLP/LLM + + # Default personas based on stakeholders + personas = self._extract_personas(epic_data) + + # Generate stories from acceptance criteria + for i, criterion in enumerate(epic_data.get("acceptance_criteria", [])[:max_stories]): + persona = personas[i % len(personas)] if personas else "User" + + story = { + "title": f"{persona} can {self._generate_story_title(criterion)}", + "persona": f"As a {persona}", + "goal": self._extract_goal(criterion), + "benefit": self._generate_benefit(criterion, epic_data), + "acceptance_criteria": [ + criterion, + f"The implementation is tested and verified", + f"Documentation is updated to reflect changes" + ] + } + + stories.append(story) + + # If we don't have enough stories from AC, generate from epic content + if len(stories) < max_stories: + additional_stories = self._generate_additional_stories( + epic_data, + max_stories - len(stories), + personas + ) + stories.extend(additional_stories) + + return stories[:max_stories] + + def _extract_personas(self, epic_data: Dict[str, Any]) -> List[str]: + """Extract or infer personas from Epic data""" + personas = [] + + # Use stakeholders as personas + stakeholders = epic_data.get("stakeholders", []) + for sh in stakeholders: + # Extract persona from stakeholder name + # e.g., "Product Team" -> "Product Manager" + if "team" in sh.lower(): + personas.append(sh.replace(" Team", " Member").replace(" team", " member")) + else: + personas.append(sh) + + # Default personas if none found + if not personas: + personas = ["User", "Administrator", "Developer"] + + return personas + + def _generate_story_title(self, criterion: str) -> str: + """Generate a story title from acceptance criterion""" + # Remove common prefixes and convert to action + title = criterion.lower() + title = re.sub(r'^(all|the|a|an)\s+', '', title) + + # Limit length + words = title.split()[:8] + return ' '.join(words) + + def _extract_goal(self, criterion: str) -> str: + """Extract the goal from acceptance criterion""" + # Simple extraction - in real system, use NLP + return criterion + + def _generate_benefit(self, criterion: str, epic_data: Dict[str, Any]) -> str: + """Generate benefit statement from criterion and epic context""" + # Use summary as context for benefit + summary = epic_data.get("summary", "") + if summary: + # Extract first key phrase + benefit_phrases = summary.split('.') + if benefit_phrases: + return f"So that {benefit_phrases[0].strip().lower()}" + + return "So that the business objectives are met" + + def _generate_additional_stories(self, epic_data: Dict[str, Any], + count: int, personas: List[str]) -> List[Dict[str, Any]]: + """ + Generate additional stories if we don't have enough from acceptance criteria + + Args: + epic_data: Parsed Epic data + count: Number of additional stories needed + personas: Available personas + + Returns: + List of additional user stories + """ + stories = [] + + # Generic story templates based on epic title and background + title = epic_data.get("title", "feature") + background = epic_data.get("background", "") + + # Extract key capabilities from background + capabilities = self._extract_capabilities(background) + + for i in range(min(count, len(capabilities))): + capability = capabilities[i] + persona = personas[i % len(personas)] if personas else "User" + + story = { + "title": f"{persona} can {capability}", + "persona": f"As a {persona}", + "goal": capability, + "benefit": f"So that {title.lower()} is achieved", + "acceptance_criteria": [ + f"{capability} functionality is implemented", + f"User can successfully {capability.lower()}", + f"Changes are tested and documented" + ] + } + + stories.append(story) + + return stories + + def _extract_capabilities(self, text: str) -> List[str]: + """Extract key capabilities from text""" + # Simple heuristic: look for verb phrases + capabilities = [] + + # Common capability patterns + patterns = [ + r'enable.+?to (\w+\s+\w+)', + r'allow.+?to (\w+\s+\w+)', + r'provide (\w+\s+\w+)', + r'implement (\w+\s+\w+)', + ] + + for pattern in patterns: + matches = re.findall(pattern, text, re.IGNORECASE) + capabilities.extend(matches) + + # Default capabilities if none found + if not capabilities: + capabilities = [ + "access the system", + "view their data", + "manage their account", + "receive notifications", + "generate reports" + ] + + return capabilities + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Take an Epic (as Markdown) and decompose it into user stories. Analyzes Epic document and identifies major deliverables, grouping them by persona or capability." + ) + + parser.add_argument( + "--epic-file", + required=True, + help="Path to the epic.md file to decompose" + ) + parser.add_argument( + "--max-stories", + type=int, + default=5, + help="Maximum number of stories to generate (default: 5)" + ) + parser.add_argument( + "--output-path", + default="./stories.json", + help="Where to save the stories.json file (default: ./stories.json)" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = EpicDecompose() + + # Execute skill + result = skill.execute( + epic_file=args.epic_file, + max_stories=args.max_stories, + output_path=args.output_path, + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/epic.decompose/skill.yaml b/skills/epic.decompose/skill.yaml new file mode 100644 index 0000000..17bedd8 --- /dev/null +++ b/skills/epic.decompose/skill.yaml @@ -0,0 +1,28 @@ +name: epic.decompose +version: 0.1.0 +description: Take an Epic (as Markdown) and decompose it into user stories. Analyzes + Epic document and identifies major deliverables, grouping them by persona or capability. +inputs: +- 'epic_file (string, required): Path to the epic.md file to decompose' +- 'max_stories (integer, optional): Maximum number of stories to generate (default: + 5)' +- 'output_path (string, optional): Where to save the stories.json file (default: ./stories.json)' +outputs: +- 'stories.json: Structured JSON list of story summaries with persona, goal, benefit, + and acceptance criteria' +status: active +permissions: +- filesystem:read +- filesystem:write +entrypoints: +- command: /epic/decompose + handler: epic_decompose.py + runtime: python + description: Take an Epic (as Markdown) and decompose it into user stories. Analyzes + Epic document and identifies +artifact_metadata: + produces: + - type: user-stories-list + consumes: + - type: agile-epic + required: true diff --git a/skills/epic.decompose/test_epic_decompose.py b/skills/epic.decompose/test_epic_decompose.py new file mode 100644 index 0000000..81475f2 --- /dev/null +++ b/skills/epic.decompose/test_epic_decompose.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for epic.decompose + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.epic_decompose import epic_decompose + + +class TestEpicDecompose: + """Tests for EpicDecompose""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = epic_decompose.EpicDecompose() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["epic_decompose.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + epic_decompose.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Take an Epic (as Markdown) and decompose it into u" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/epic.write/SKILL.md b/skills/epic.write/SKILL.md new file mode 100644 index 0000000..f43e0da --- /dev/null +++ b/skills/epic.write/SKILL.md @@ -0,0 +1,84 @@ +# epic.write + +Generate an Agile Epic from a high-level goal, feature request, or strategic initiative. Creates structured Agile Epic document that follows best practices. + +## Overview + +**Purpose:** Generate an Agile Epic from a high-level goal, feature request, or strategic initiative. Creates structured Agile Epic document that follows best practices. + +**Command:** `/epic/write` + +## Usage + +### Basic Usage + +```bash +python3 skills/epic/write/epic_write.py +``` + +### With Arguments + +```bash +python3 skills/epic/write/epic_write.py \ + --initiative_name_(string,_required):_the_overarching_initiative_or_product_goal "value" \ + --context_(string,_required):_relevant_background,_rationale,_and_success_criteria "value" \ + --stakeholders_(array_of_strings,_required):_who_cares_about_this_and_why "value" \ + --output_path_(string,_optional):_where_to_save_the_epic.md_file_(default:_./epic.md) "value" \ + --output-format json +``` + +## Inputs + +- **initiative_name (string, required): The overarching initiative or product goal** +- **context (string, required): Relevant background, rationale, and success criteria** +- **stakeholders (array of strings, required): Who cares about this and why** +- **output_path (string, optional): Where to save the epic.md file (default: ./epic.md)** + +## Outputs + +- **epic.md: Markdown file with structured Epic fields (title, summary, background, acceptance criteria, stakeholders, next steps)** + +## Artifact Metadata + +### Produces + +- `agile-epic` + +## Examples + +- python3 skills/epic.write/epic_write.py --initiative-name "Customer Self-Service Portal" --context "Enable customers to manage accounts" --stakeholders "Product Team,Engineering" + +## Permissions + +- `filesystem:read` +- `filesystem:write` + +## Implementation Notes + +Validate all required inputs are provided. Generate clear, measurable acceptance criteria. Format output following Agile Epic standards. Include metadata for artifact tracking. Provide clear guidance on next step (epic.decompose). + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - epic.write +``` + +## Testing + +Run tests with: + +```bash +pytest skills/epic/write/test_epic_write.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/epic.write/__init__.py b/skills/epic.write/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/epic.write/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/epic.write/epic_write.py b/skills/epic.write/epic_write.py new file mode 100755 index 0000000..fd46296 --- /dev/null +++ b/skills/epic.write/epic_write.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +""" +epic.write - Generate an Agile Epic from a high-level goal, feature request, or strategic initiative. Creates structured Agile Epic document that follows best practices. + +Generated by meta.skill with Betty Framework certification +""" + +import os +import sys +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional +from datetime import datetime + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.certification import certified_skill + +logger = setup_logger(__name__) + + +class EpicWrite: + """ + Generate an Agile Epic from a high-level goal, feature request, or strategic initiative. Creates structured Agile Epic document that follows best practices. + """ + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + @certified_skill("epic.write") + def execute(self, initiative_name: Optional[str] = None, context: Optional[str] = None, + stakeholders: Optional[str] = None, output_path: Optional[str] = None) -> Dict[str, Any]: + """ + Execute the skill + + Args: + initiative_name: The overarching initiative or product goal + context: Relevant background, rationale, and success criteria + stakeholders: Who cares about this and why (comma-separated) + output_path: Where to save the epic.md file (default: ./epic.md) + + Returns: + Dict with execution results + """ + try: + logger.info("Executing epic.write...") + + # Validate required inputs + if not initiative_name: + raise ValueError("initiative_name is required") + if not context: + raise ValueError("context is required") + if not stakeholders: + raise ValueError("stakeholders is required") + + # Set default output path + if not output_path: + output_path = "./epic.md" + + # Parse stakeholders (comma-separated) + stakeholder_list = [s.strip() for s in stakeholders.split(',')] + + # Generate Epic content + epic_content = self._generate_epic(initiative_name, context, stakeholder_list) + + # Write to file + output_file = Path(output_path) + output_file.parent.mkdir(parents=True, exist_ok=True) + output_file.write_text(epic_content) + + logger.info(f"Epic written to {output_path}") + + result = { + "ok": True, + "status": "success", + "message": f"Epic '{initiative_name}' created successfully", + "output_file": str(output_path), + "artifact_type": "agile-epic", + "next_step": "Use epic.decompose to break this Epic into user stories" + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + def _generate_epic(self, initiative_name: str, context: str, stakeholders: List[str]) -> str: + """ + Generate the Epic markdown content + + Args: + initiative_name: Name of the initiative + context: Background and context + stakeholders: List of stakeholders + + Returns: + Formatted Epic markdown + """ + # Extract or generate acceptance criteria from context + acceptance_criteria = self._generate_acceptance_criteria(context) + + epic_md = f"""# Epic: {initiative_name} + +## Summary +{self._generate_summary(initiative_name, context)} + +## Background +{context} + +## Acceptance Criteria +{acceptance_criteria} + +## Stakeholders +{self._format_stakeholders(stakeholders)} + +## Next Steps +Pass to skill: `epic.decompose` + +This Epic should be decomposed into individual user stories using the `epic.decompose` skill. + +```bash +python3 skills/epic.decompose/epic_decompose.py \\ + --epic-file {Path(initiative_name.lower().replace(' ', '_') + '_epic.md').name} \\ + --max-stories 5 +``` + +--- + +**Epic Metadata** +- Created: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +- Type: agile-epic +- Version: 1.0 +- Status: Draft +""" + return epic_md + + def _generate_summary(self, initiative_name: str, context: str) -> str: + """Generate a one-sentence business objective""" + # Simple heuristic: use first sentence of context or generate from name + sentences = context.split('.') + if sentences: + return sentences[0].strip() + '.' + return f"Enable {initiative_name.lower()} capabilities for the organization." + + def _generate_acceptance_criteria(self, context: str) -> str: + """Generate measurable acceptance criteria from context""" + # Extract key outcomes or generate default criteria + criteria = [ + "All requirements defined in the Epic are implemented and tested", + "Stakeholder approval obtained for the implementation", + "Documentation is complete and accurate", + "Solution meets performance and quality standards" + ] + + return '\n'.join(f"- [ ] {c}" for c in criteria) + + def _format_stakeholders(self, stakeholders: List[str]) -> str: + """Format stakeholders list""" + return '\n'.join(f"- **{s}**" for s in stakeholders) + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Generate an Agile Epic from a high-level goal, feature request, or strategic initiative. Creates structured Agile Epic document that follows best practices." + ) + + parser.add_argument( + "--initiative-name", + required=True, + help="The overarching initiative or product goal" + ) + parser.add_argument( + "--context", + required=True, + help="Relevant background, rationale, and success criteria" + ) + parser.add_argument( + "--stakeholders", + required=True, + help="Who cares about this and why (comma-separated)" + ) + parser.add_argument( + "--output-path", + default="./epic.md", + help="Where to save the epic.md file (default: ./epic.md)" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = EpicWrite() + + # Execute skill + result = skill.execute( + initiative_name=args.initiative_name, + context=args.context, + stakeholders=args.stakeholders, + output_path=args.output_path, + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/epic.write/skill.yaml b/skills/epic.write/skill.yaml new file mode 100644 index 0000000..30ee5a0 --- /dev/null +++ b/skills/epic.write/skill.yaml @@ -0,0 +1,25 @@ +name: epic.write +version: 0.1.0 +description: Generate an Agile Epic from a high-level goal, feature request, or strategic + initiative. Creates structured Agile Epic document that follows best practices. +inputs: +- 'initiative_name (string, required): The overarching initiative or product goal' +- 'context (string, required): Relevant background, rationale, and success criteria' +- 'stakeholders (array of strings, required): Who cares about this and why' +- 'output_path (string, optional): Where to save the epic.md file (default: ./epic.md)' +outputs: +- 'epic.md: Markdown file with structured Epic fields (title, summary, background, + acceptance criteria, stakeholders, next steps)' +status: active +permissions: +- filesystem:read +- filesystem:write +entrypoints: +- command: /epic/write + handler: epic_write.py + runtime: python + description: Generate an Agile Epic from a high-level goal, feature request, or + strategic initiative. Creates str +artifact_metadata: + produces: + - type: agile-epic diff --git a/skills/epic.write/test_epic_write.py b/skills/epic.write/test_epic_write.py new file mode 100644 index 0000000..31703d8 --- /dev/null +++ b/skills/epic.write/test_epic_write.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for epic.write + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.epic_write import epic_write + + +class TestEpicWrite: + """Tests for EpicWrite""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = epic_write.EpicWrite() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["epic_write.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + epic_write.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Generate an Agile Epic from a high-level goal, fea" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/file.compare/README.md b/skills/file.compare/README.md new file mode 100644 index 0000000..13befb2 --- /dev/null +++ b/skills/file.compare/README.md @@ -0,0 +1,77 @@ +# file.compare + +Compare two files and generate detailed diff reports showing line-by-line differences + +## Overview + +**Purpose:** Compare two files and generate detailed diff reports showing line-by-line differences + +**Command:** `/file/compare` + +## Usage + +### Basic Usage + +```bash +python3 skills/file/compare/file_compare.py +``` + +### With Arguments + +```bash +python3 skills/file/compare/file_compare.py \ + --file_path_1 "value" \ + --file_path_2 "value" \ + --output_format_(optional) "value" \ + --output-format json +``` + +## Inputs + +- **file_path_1** +- **file_path_2** +- **output_format (optional)** + +## Outputs + +- **diff_report.json** + +## Artifact Metadata + +### Produces + +- `diff-report` + +## Permissions + +- `filesystem:read` + +## Implementation Notes + +Use Python's difflib to compare files line by line. Support multiple output formats: - unified: Standard unified diff format - context: Context diff format - html: HTML diff with color coding - json: Structured JSON with line-by-line changes Include statistics: - Total lines added - Total lines removed - Total lines modified - Percentage similarity Handle binary files by detecting and reporting as non-text. + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - file.compare +``` + +## Testing + +Run tests with: + +```bash +pytest skills/file/compare/test_file_compare.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/file.compare/__init__.py b/skills/file.compare/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/file.compare/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/file.compare/file_compare.py b/skills/file.compare/file_compare.py new file mode 100755 index 0000000..f7017b6 --- /dev/null +++ b/skills/file.compare/file_compare.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +file.compare - Compare two files and generate detailed diff reports showing line-by-line differences + +Generated by meta.skill +""" + +import os +import sys +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +class FileCompare: + """ + Compare two files and generate detailed diff reports showing line-by-line differences + """ + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + def execute(self, file_path_1: Optional[str] = None, file_path_2: Optional[str] = None, output_format_optional: Optional[str] = None) -> Dict[str, Any]: + """ + Execute the skill + + Returns: + Dict with execution results + """ + try: + logger.info("Executing file.compare...") + + # TODO: Implement skill logic here + + # Implementation notes: + # Use Python's difflib to compare files line by line. Support multiple output formats: - unified: Standard unified diff format - context: Context diff format - html: HTML diff with color coding - json: Structured JSON with line-by-line changes Include statistics: - Total lines added - Total lines removed - Total lines modified - Percentage similarity Handle binary files by detecting and reporting as non-text. + + # Placeholder implementation + result = { + "ok": True, + "status": "success", + "message": "Skill executed successfully" + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Compare two files and generate detailed diff reports showing line-by-line differences" + ) + + parser.add_argument( + "--file-path-1", + help="file_path_1" + ) + parser.add_argument( + "--file-path-2", + help="file_path_2" + ) + parser.add_argument( + "--output-format-optional", + help="output_format (optional)" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = FileCompare() + + # Execute skill + result = skill.execute( + file_path_1=args.file_path_1, + file_path_2=args.file_path_2, + output_format_optional=args.output_format_optional, + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/file.compare/skill.yaml b/skills/file.compare/skill.yaml new file mode 100644 index 0000000..573de51 --- /dev/null +++ b/skills/file.compare/skill.yaml @@ -0,0 +1,22 @@ +name: file.compare +version: 0.1.0 +description: Compare two files and generate detailed diff reports showing line-by-line + differences +inputs: +- file_path_1 +- file_path_2 +- output_format (optional) +outputs: +- diff_report.json +status: active +permissions: +- filesystem:read +entrypoints: +- command: /file/compare + handler: file_compare.py + runtime: python + description: Compare two files and generate detailed diff reports showing line-by-line + differences +artifact_metadata: + produces: + - type: diff-report diff --git a/skills/file.compare/test_file_compare.py b/skills/file.compare/test_file_compare.py new file mode 100644 index 0000000..cc88bee --- /dev/null +++ b/skills/file.compare/test_file_compare.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for file.compare + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.file_compare import file_compare + + +class TestFileCompare: + """Tests for FileCompare""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = file_compare.FileCompare() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["file_compare.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + file_compare.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Compare two files and generate detailed diff repor" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/generate.docs/SKILL.md b/skills/generate.docs/SKILL.md new file mode 100644 index 0000000..a13f405 --- /dev/null +++ b/skills/generate.docs/SKILL.md @@ -0,0 +1,304 @@ +# generate.docs + +## Overview + +**generate.docs** automatically generates or updates SKILL.md documentation from skill.yaml manifest files, ensuring consistent and comprehensive documentation across all Betty skills. + +## Purpose + +Eliminate manual documentation drift by: +- **Before**: Developers manually write and update SKILL.md files, leading to inconsistency and outdated docs +- **After**: Documentation is automatically generated from the authoritative skill.yaml manifest + +This skill helps maintain high-quality documentation by: +- Reading skill.yaml manifest files +- Extracting inputs, outputs, and metadata +- Creating standardized SKILL.md documentation +- Ensuring consistency across all Betty skills +- Supporting dry-run previews and safe overwrites + +## Usage + +### Basic Usage + +```bash +python skills/generate.docs/generate_docs.py [options] +``` + +### Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `manifest_path` | string | Yes | - | Path to skill.yaml manifest file to generate documentation from | +| `--overwrite` | boolean | No | `false` | Overwrite existing SKILL.md file if it exists | +| `--dry-run` | boolean | No | `false` | Preview the generated documentation without writing to disk | +| `--output-path` | string | No | - | Custom output path for SKILL.md (defaults to same directory as manifest) | + +## Outputs + +| Output | Type | Description | +|--------|------|-------------| +| `doc_path` | string | Path to generated or updated SKILL.md file | +| `doc_content` | string | Generated documentation content | +| `dry_run_preview` | string | Preview of documentation (when dry_run=true) | + +## Examples + +### Example 1: Generate Documentation for a New Skill + +```bash +python skills/generate.docs/generate_docs.py skills/api.define/skill.yaml +``` + +**Output**: Creates `skills/api.define/SKILL.md` with comprehensive documentation + +**Result**: +```json +{ + "status": "success", + "data": { + "doc_path": "skills/api.define/SKILL.md", + "skill_name": "api.define", + "dry_run": false + } +} +``` + +### Example 2: Preview Documentation Without Writing + +```bash +python skills/generate.docs/generate_docs.py \ + skills/hook.define/skill.yaml \ + --dry-run=true +``` + +**Result**: Prints formatted documentation preview to console without creating any files + +``` +================================================================================ +DRY RUN PREVIEW +================================================================================ +# hook.define + +## Overview +... +================================================================================ +``` + +### Example 3: Overwrite Existing Documentation + +```bash +python skills/generate.docs/generate_docs.py \ + skills/api.validate/skill.yaml \ + --overwrite=true +``` + +**Result**: Replaces existing `skills/api.validate/SKILL.md` with newly generated version + +### Example 4: Custom Output Path + +```bash +python skills/generate.docs/generate_docs.py \ + skills/workflow.compose/skill.yaml \ + --output-path=docs/skills/workflow-compose.md +``` + +**Result**: Generates documentation at `docs/skills/workflow-compose.md` instead of default location + +### Example 5: Batch Documentation Generation + +```bash +# Generate docs for all skills in the repository +for manifest in skills/*/skill.yaml; do + echo "Generating docs for $manifest..." + python skills/generate.docs/generate_docs.py "$manifest" --overwrite=true +done +``` + +**Result**: Updates documentation for all skills, ensuring consistency across the entire framework + +## Generated Documentation Structure + +The generated SKILL.md includes the following sections: + +1. **Overview** - Skill name and brief description from manifest +2. **Purpose** - Detailed explanation of what the skill does +3. **Usage** - Command-line usage examples with proper syntax +4. **Parameters** - Detailed table of all inputs with types, requirements, and defaults +5. **Outputs** - Description of all skill outputs +6. **Usage Template** - Practical examples showing common use cases +7. **Integration Notes** - How to use with workflows, other skills, and batch operations +8. **Dependencies** - Required dependencies from manifest +9. **Tags** - Skill tags for categorization +10. **Version** - Skill version from manifest + +## Integration Notes + +### Use in Workflows + +```yaml +# workflows/maintain_docs.yaml +name: Documentation Maintenance +description: Keep all skill documentation up to date + +steps: + - name: Update skill docs + skill: generate.docs + args: + - "${skill_manifest_path}" + - "--overwrite=true" + + - name: Commit changes + command: git add ${doc_path} && git commit -m "docs: update skill documentation" +``` + +### Use with skill.create + +When creating a new skill, automatically generate its documentation: + +```bash +# Create new skill +python skills/skill.create/skill_create.py my.new.skill + +# Generate documentation +python skills/generate.docs/generate_docs.py \ + skills/my.new.skill/skill.yaml +``` + +### Integration with CI/CD + +Add to your CI pipeline to ensure documentation stays in sync: + +```yaml +# .github/workflows/docs.yml +- name: Check documentation is up to date + run: | + for manifest in skills/*/skill.yaml; do + python skills/generate.docs/generate_docs.py "$manifest" --dry-run=true > /tmp/preview.md + skill_dir=$(dirname "$manifest") + if ! diff -q "$skill_dir/SKILL.md" /tmp/preview.md; then + echo "Documentation out of sync for $manifest" + exit 1 + fi + done +``` + +### Use with Hooks + +Automatically regenerate docs when skill.yaml is modified: + +```bash +python skills/hook.define/hook_define.py \ + on_file_save \ + "python skills/generate.docs/generate_docs.py {file_path} --overwrite=true" \ + --pattern="*/skill.yaml" \ + --blocking=false \ + --description="Auto-regenerate SKILL.md when skill.yaml changes" +``` + +## Benefits + +### For Developers +- No manual documentation writing +- Consistent format across all skills +- Preview changes before committing +- Safe overwrite protection + +### For Teams +- Single source of truth (skill.yaml) +- Automated documentation updates +- Standardized skill documentation +- Easy onboarding with clear docs + +### For Maintenance +- Detect documentation drift +- Batch regeneration for all skills +- CI/CD integration for validation +- Version-controlled documentation + +## Error Handling + +### Manifest Not Found + +```bash +$ python skills/generate.docs/generate_docs.py nonexistent/skill.yaml +``` + +**Error**: +```json +{ + "status": "error", + "error": "Manifest file not found: nonexistent/skill.yaml" +} +``` + +### File Already Exists (Without Overwrite) + +```bash +$ python skills/generate.docs/generate_docs.py skills/api.define/skill.yaml +``` + +**Error**: +```json +{ + "status": "error", + "error": "SKILL.md already exists at skills/api.define/SKILL.md. Use --overwrite=true to replace it or --dry-run=true to preview." +} +``` + +### Invalid YAML Manifest + +```bash +$ python skills/generate.docs/generate_docs.py broken-skill.yaml +``` + +**Error**: +```json +{ + "status": "error", + "error": "Failed to parse YAML manifest: ..." +} +``` + +## Customization + +After generation, you can manually enhance the documentation: + +1. **Add detailed examples** - The generated docs include basic examples; add more complex ones +2. **Include diagrams** - Add architecture or flow diagrams +3. **Expand integration notes** - Add specific team or project integration details +4. **Add troubleshooting** - Document common issues and solutions + +**Note**: Manual changes will be overwritten if you regenerate with `--overwrite=true`. Consider adding custom sections to the generator or maintaining separate docs for detailed content. + +## Best Practices + +1. **Run in dry-run mode first** - Preview changes before writing +2. **Use version control** - Track documentation changes via git +3. **Regenerate after manifest changes** - Keep docs in sync with manifest +4. **Include in PR reviews** - Ensure manifest and docs are updated together +5. **Automate in CI** - Validate docs match manifests in automated checks + +## Dependencies + +- **PyYAML**: Required for YAML manifest parsing + ```bash + pip install pyyaml + ``` + +- **context.schema**: For validation rule definitions + +## Files Created + +- `SKILL.md` - Generated skill documentation (in same directory as skill.yaml by default) + +## See Also + +- [skill.create](../skill.create/SKILL.md) - Create new skills +- [skill.define](../skill.define/SKILL.md) - Define skill manifests +- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model +- [Skill Development Guide](../../docs/skill-development.md) - Creating new skills + +## Version + +**0.1.0** - Initial implementation with manifest-to-markdown generation diff --git a/skills/generate.docs/__init__.py b/skills/generate.docs/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/generate.docs/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/generate.docs/generate_docs.py b/skills/generate.docs/generate_docs.py new file mode 100755 index 0000000..ab1a58c --- /dev/null +++ b/skills/generate.docs/generate_docs.py @@ -0,0 +1,540 @@ +#!/usr/bin/env python3 +""" +Generate or update SKILL.md documentation from skill.yaml manifest files. + +This skill automatically creates comprehensive documentation for Betty skills +based on their manifest definitions, ensuring consistency and completeness. +""" + +import sys +import json +import argparse +import os +from pathlib import Path +from typing import Dict, Any, List, Optional + +# Add betty module to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response, BettyError +from betty.validation import validate_path + +logger = setup_logger(__name__) + + +def load_skill_manifest(manifest_path: str) -> Dict[str, Any]: + """ + Load and parse a skill.yaml manifest file. + + Args: + manifest_path: Path to skill.yaml file + + Returns: + Parsed manifest data + + Raises: + BettyError: If manifest file is invalid or not found + """ + manifest_file = Path(manifest_path) + + if not manifest_file.exists(): + raise BettyError(f"Manifest file not found: {manifest_path}") + + if not manifest_file.is_file(): + raise BettyError(f"Manifest path is not a file: {manifest_path}") + + try: + import yaml + with open(manifest_file, 'r') as f: + manifest = yaml.safe_load(f) + + if not isinstance(manifest, dict): + raise BettyError("Manifest must be a YAML object/dictionary") + + logger.info(f"Loaded skill manifest from {manifest_path}") + return manifest + except yaml.YAMLError as e: + raise BettyError(f"Failed to parse YAML manifest: {e}") + except Exception as e: + raise BettyError(f"Failed to load manifest: {e}") + + +def normalize_input(inp: Any) -> Dict[str, Any]: + """ + Normalize input definition to standard format. + + Args: + inp: Input definition (string or dict) + + Returns: + Normalized input dictionary + """ + if isinstance(inp, str): + # Simple string format: "workflow_path" + return { + 'name': inp, + 'type': 'any', + 'required': True, + 'description': 'No description' + } + elif isinstance(inp, dict): + # Already in object format + return inp + else: + return { + 'name': 'unknown', + 'type': 'any', + 'required': False, + 'description': 'Invalid input format' + } + + +def format_inputs_section(inputs: List[Any]) -> str: + """ + Format the inputs section for the documentation. + + Args: + inputs: List of input definitions from manifest (strings or dicts) + + Returns: + Formatted markdown table + """ + if not inputs: + return "_No inputs defined_\n" + + lines = [ + "| Parameter | Type | Required | Default | Description |", + "|-----------|------|----------|---------|-------------|" + ] + + for inp in inputs: + normalized = normalize_input(inp) + name = normalized.get('name', 'unknown') + type_val = normalized.get('type', 'any') + required = 'Yes' if normalized.get('required', False) else 'No' + default = normalized.get('default', '-') + if default is True: + default = 'true' + elif default is False: + default = 'false' + elif default != '-': + default = f'`{default}`' + description = normalized.get('description', 'No description') + + lines.append(f"| `{name}` | {type_val} | {required} | {default} | {description} |") + + return '\n'.join(lines) + '\n' + + +def normalize_output(out: Any) -> Dict[str, Any]: + """ + Normalize output definition to standard format. + + Args: + out: Output definition (string or dict) + + Returns: + Normalized output dictionary + """ + if isinstance(out, str): + # Simple string format: "validation_result.json" + return { + 'name': out, + 'type': 'any', + 'description': 'No description' + } + elif isinstance(out, dict): + # Already in object format + return out + else: + return { + 'name': 'unknown', + 'type': 'any', + 'description': 'Invalid output format' + } + + +def format_outputs_section(outputs: List[Any]) -> str: + """ + Format the outputs section for the documentation. + + Args: + outputs: List of output definitions from manifest (strings or dicts) + + Returns: + Formatted markdown table + """ + if not outputs: + return "_No outputs defined_\n" + + lines = [ + "| Output | Type | Description |", + "|--------|------|-------------|" + ] + + for out in outputs: + normalized = normalize_output(out) + name = normalized.get('name', 'unknown') + type_val = normalized.get('type', 'any') + description = normalized.get('description', 'No description') + + lines.append(f"| `{name}` | {type_val} | {description} |") + + return '\n'.join(lines) + '\n' + + +def generate_usage_template(manifest: Dict[str, Any]) -> str: + """ + Generate a usage template based on the skill manifest. + + Args: + manifest: Parsed skill manifest + + Returns: + Usage template string + """ + skill_name = manifest.get('name', 'skill') + inputs = manifest.get('inputs', []) + + # Get the handler/script name + entrypoints = manifest.get('entrypoints', []) + if entrypoints: + handler = entrypoints[0].get('handler', f'{skill_name.replace(".", "_")}.py') + else: + handler = f'{skill_name.replace(".", "_")}.py' + + # Build basic usage + skill_dir = f"skills/{skill_name}" + usage = f"```bash\npython {skill_dir}/{handler}" + + # Normalize inputs + normalized_inputs = [normalize_input(inp) for inp in inputs] + + # Add required positional arguments + required_inputs = [inp for inp in normalized_inputs if inp.get('required', False)] + for inp in required_inputs: + usage += f" <{inp['name']}>" + + # Add optional arguments hint + if any(not inp.get('required', False) for inp in normalized_inputs): + usage += " [options]" + + usage += "\n```" + + return usage + + +def generate_parameters_detail(inputs: List[Any]) -> str: + """ + Generate detailed parameter documentation. + + Args: + inputs: List of input definitions (strings or dicts) + + Returns: + Formatted parameter details + """ + if not inputs: + return "" + + lines = [] + for inp in inputs: + normalized = normalize_input(inp) + name = normalized.get('name', 'unknown') + description = normalized.get('description', 'No description') + type_val = normalized.get('type', 'any') + required = normalized.get('required', False) + default = normalized.get('default') + + detail = f"- `--{name}` ({type_val})" + if required: + detail += " **[Required]**" + detail += f": {description}" + if default is not None: + detail += f" (default: `{default}`)" + + lines.append(detail) + + return '\n'.join(lines) + '\n' + + +def generate_skill_documentation(manifest: Dict[str, Any]) -> str: + """ + Generate complete SKILL.md documentation from manifest. + + Args: + manifest: Parsed skill manifest + + Returns: + Generated markdown documentation + """ + name = manifest.get('name', 'Unknown Skill') + description = manifest.get('description', 'No description available') + version = manifest.get('version', '0.1.0') + inputs = manifest.get('inputs', []) + outputs = manifest.get('outputs', []) + tags = manifest.get('tags', []) + dependencies = manifest.get('dependencies', []) + + # Build documentation + doc = f"""# {name} + +## Overview + +**{name}** {description} + +## Purpose + +{description} + +This skill automatically generates documentation by: +- Reading skill.yaml manifest files +- Extracting inputs, outputs, and metadata +- Creating standardized SKILL.md documentation +- Ensuring consistency across all Betty skills + +## Usage + +### Basic Usage + +{generate_usage_template(manifest)} + +### Parameters + +{format_inputs_section(inputs)} + +## Outputs + +{format_outputs_section(outputs)} + +## Usage Template + +### Example: Generate Documentation for a Skill + +```bash +python skills/{name}/{'generate_docs.py' if '.' in name else name + '.py'} path/to/skill.yaml +``` + +### Example: Preview Without Writing + +```bash +python skills/{name}/{'generate_docs.py' if '.' in name else name + '.py'} \\ + path/to/skill.yaml \\ + --dry-run=true +``` + +### Example: Overwrite Existing Documentation + +```bash +python skills/{name}/{'generate_docs.py' if '.' in name else name + '.py'} \\ + path/to/skill.yaml \\ + --overwrite=true +``` + +## Integration Notes + +### Use in Workflows + +```yaml +# workflows/documentation.yaml +steps: + - skill: {name} + args: + - "skills/my.skill/skill.yaml" + - "--overwrite=true" +``` + +### Use with Other Skills + +```bash +# Generate documentation for a newly created skill +python skills/skill.create/skill_create.py my.new.skill +python skills/{name}/{'generate_docs.py' if '.' in name else name + '.py'} skills/my.new.skill/skill.yaml +``` + +### Batch Documentation Generation + +```bash +# Generate docs for all skills +for manifest in skills/*/skill.yaml; do + python skills/{name}/{'generate_docs.py' if '.' in name else name + '.py'} "$manifest" --overwrite=true +done +``` + +## Output Structure + +The generated SKILL.md includes: + +1. **Overview** - Skill name and brief description +2. **Purpose** - Detailed explanation of what the skill does +3. **Usage** - Command-line usage examples +4. **Parameters** - Detailed input parameter documentation +5. **Outputs** - Description of skill outputs +6. **Usage Template** - Practical examples +7. **Integration Notes** - How to use with workflows and other skills + +## Dependencies + +""" + + if dependencies: + for dep in dependencies: + doc += f"- **{dep}**: Required dependency\n" + else: + doc += "_No external dependencies_\n" + + doc += "\n## Tags\n\n" + if tags: + doc += ', '.join(f'`{tag}`' for tag in tags) + '\n' + else: + doc += "_No tags defined_\n" + + doc += f""" +## See Also + +- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model +- [Skill Development Guide](../../docs/skill-development.md) - Creating new skills + +## Version + +**{version}** - Generated documentation from skill manifest +""" + + return doc + + +def generate_docs( + manifest_path: str, + overwrite: bool = False, + dry_run: bool = False, + output_path: Optional[str] = None +) -> Dict[str, Any]: + """ + Generate SKILL.md documentation from a skill manifest. + + Args: + manifest_path: Path to skill.yaml file + overwrite: Whether to overwrite existing SKILL.md + dry_run: Preview without writing + output_path: Custom output path (optional) + + Returns: + Result dictionary with doc path and content + + Raises: + BettyError: If manifest is invalid or file operations fail + """ + # Load manifest + manifest = load_skill_manifest(manifest_path) + + # Generate documentation + doc_content = generate_skill_documentation(manifest) + + # Determine output path + if output_path: + doc_path = Path(output_path) + else: + # Default to same directory as manifest + manifest_file = Path(manifest_path) + doc_path = manifest_file.parent / "SKILL.md" + + # Check if file exists and overwrite is False + if doc_path.exists() and not overwrite and not dry_run: + raise BettyError( + f"SKILL.md already exists at {doc_path}. " + f"Use --overwrite=true to replace it or --dry-run=true to preview." + ) + + result = { + "doc_path": str(doc_path), + "doc_content": doc_content, + "skill_name": manifest.get('name', 'unknown'), + "dry_run": dry_run + } + + if dry_run: + result["dry_run_preview"] = doc_content + logger.info(f"DRY RUN: Would write documentation to {doc_path}") + print("\n" + "="*80) + print("DRY RUN PREVIEW") + print("="*80) + print(doc_content) + print("="*80) + return result + + # Write documentation to file + try: + doc_path.parent.mkdir(parents=True, exist_ok=True) + with open(doc_path, 'w') as f: + f.write(doc_content) + logger.info(f"Generated SKILL.md at {doc_path}") + except Exception as e: + raise BettyError(f"Failed to write documentation: {e}") + + return result + + +def main(): + parser = argparse.ArgumentParser( + description="Generate or update SKILL.md documentation from skill.yaml manifest" + ) + parser.add_argument( + "manifest_path", + type=str, + help="Path to skill.yaml manifest file" + ) + parser.add_argument( + "--overwrite", + type=lambda x: x.lower() in ['true', '1', 'yes'], + default=False, + help="Overwrite existing SKILL.md file (default: false)" + ) + parser.add_argument( + "--dry-run", + type=lambda x: x.lower() in ['true', '1', 'yes'], + default=False, + help="Preview without writing to disk (default: false)" + ) + parser.add_argument( + "--output-path", + type=str, + help="Custom output path for SKILL.md (optional)" + ) + + args = parser.parse_args() + + try: + # Check if PyYAML is installed + try: + import yaml + except ImportError: + raise BettyError( + "PyYAML is required for generate.docs. Install with: pip install pyyaml" + ) + + # Generate documentation + logger.info(f"Generating documentation from {args.manifest_path}") + result = generate_docs( + manifest_path=args.manifest_path, + overwrite=args.overwrite, + dry_run=args.dry_run, + output_path=args.output_path + ) + + # Return structured result + output = { + "status": "success", + "data": result + } + + if not args.dry_run: + print(json.dumps(output, indent=2)) + + except Exception as e: + logger.error(f"Failed to generate documentation: {e}") + print(json.dumps(format_error_response(e), indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/generate.docs/skill.yaml b/skills/generate.docs/skill.yaml new file mode 100644 index 0000000..4f3112a --- /dev/null +++ b/skills/generate.docs/skill.yaml @@ -0,0 +1,54 @@ +name: generate.docs +version: 0.1.0 +description: Automatically generate or update SKILL.md documentation from skill.yaml manifests + +inputs: + - name: manifest_path + type: string + required: true + description: Path to skill.yaml manifest file to generate documentation from + + - name: overwrite + type: boolean + required: false + default: false + description: Overwrite existing SKILL.md file if it exists + + - name: dry_run + type: boolean + required: false + default: false + description: Preview the generated documentation without writing to disk + + - name: output_path + type: string + required: false + description: Custom output path for SKILL.md (defaults to same directory as manifest) + +outputs: + - name: doc_path + type: string + description: Path to generated or updated SKILL.md file + + - name: doc_content + type: string + description: Generated documentation content + + - name: dry_run_preview + type: string + description: Preview of documentation (when dry_run=true) + +dependencies: + - context.schema + +entrypoints: + - command: /skill/generate/docs + handler: generate_docs.py + runtime: python + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: [documentation, automation, scaffolding, skill-management] diff --git a/skills/generate.marketplace/SKILL.md b/skills/generate.marketplace/SKILL.md new file mode 100644 index 0000000..dda3beb --- /dev/null +++ b/skills/generate.marketplace/SKILL.md @@ -0,0 +1,724 @@ +--- +name: Generate Marketplace +description: Generate marketplace catalog files from Betty Framework registries +--- + +# generate.marketplace + +## Overview + +**generate.marketplace** generates the RiskExec Claude Marketplace catalogs by filtering certified skills, agents, commands, and hooks from the Betty Framework registries. It transforms registry entries into marketplace-ready JSON files optimized for discovery and distribution. + +## Purpose + +Automates the generation of marketplace catalogs to maintain consistency between: +- **Skill Registry** (`registry/skills.json`) - All registered skills +- **Agent Registry** (`registry/agents.json`) - All registered agents +- **Command Registry** (`registry/commands.json`) - All registered commands +- **Hook Registry** (`registry/hooks.json`) - All registered hooks +- **Skills Marketplace** (`marketplace/skills.json`) - Certified skills for distribution +- **Agents Marketplace** (`marketplace/agents.json`) - Certified agents for distribution +- **Commands Marketplace** (`marketplace/commands.json`) - Certified commands for distribution +- **Hooks Marketplace** (`marketplace/hooks.json`) - Certified hooks for distribution + +This eliminates manual curation of marketplace catalogs and ensures only production-ready, certified components are published. + +## What It Does + +1. **Reads Registries**: Loads `registry/skills.json`, `registry/agents.json`, `registry/commands.json`, and `registry/hooks.json` +2. **Filters Active Items**: Processes only entries with `status: active` +3. **Filters Certified Items**: Includes only entries with `certified: true` (if field exists) +4. **Transforms Format**: Converts registry entries to marketplace format +5. **Enriches Metadata**: Adds maintainer, usage examples, documentation URLs, and last_updated timestamps +6. **Generates Catalogs**: Outputs `marketplace/skills.json`, `marketplace/agents.json`, `marketplace/commands.json`, and `marketplace/hooks.json` +7. **Reports Statistics**: Shows certification counts and totals + +## Usage + +### Basic Usage + +```bash +python skills/generate.marketplace/generate_marketplace.py +``` + +No arguments required - reads from standard registry locations. + +### Via Betty CLI + +```bash +/marketplace/generate +``` + +### Expected Directory Structure + +``` +betty/ +├── registry/ +│ ├── skills.json # Source: All registered skills +│ ├── agents.json # Source: All registered agents +│ ├── commands.json # Source: All registered commands +│ └── hooks.json # Source: All registered hooks +└── marketplace/ + ├── skills.json # Output: Certified skills only + ├── agents.json # Output: Certified agents only + ├── commands.json # Output: Certified commands only + └── hooks.json # Output: Certified hooks only +``` + +## Filtering Logic + +### Certification Criteria + +A skill, agent, command, or hook is included in the marketplace if: + +1. **Status is Active**: `status: "active"` +2. **Certified Flag** (if present): `certified: true` + +If the `certified` field is not present, active items are considered certified by default. + +### Status Values + +| Status | Included in Marketplace | Purpose | +|--------|------------------------|---------| +| `active` | Yes | Production-ready, certified items | +| `draft` | No | Work in progress, not ready for distribution | +| `deprecated` | No | Outdated, should not be used | +| `experimental` | No | Testing phase, unstable | + +## Output Format + +### Skills Marketplace Structure + +```json +{ + "marketplace_version": "1.0.0", + "generated_at": "2025-10-23T17:51:58.579847+00:00", + "last_updated": "2025-10-23T17:51:58.579847+00:00", + "description": "Betty Framework Certified Skills Marketplace", + "total_skills": 20, + "certified_count": 16, + "draft_count": 4, + "catalog": [ + { + "name": "api.validate", + "version": "0.1.0", + "description": "Validate OpenAPI and AsyncAPI specifications", + "status": "certified", + "tags": ["api", "validation", "openapi"], + "maintainer": "Betty Core Team", + "usage_examples": [ + "Validate OpenAPI spec: /skill/api/validate --spec_path api.yaml" + ], + "documentation_url": "https://betty-framework.dev/docs/skills/api.validate", + "dependencies": ["context.schema"], + "entrypoints": [...], + "inputs": [...], + "outputs": [...] + } + ] +} +``` + +### Agents Marketplace Structure + +```json +{ + "marketplace_version": "1.0.0", + "generated_at": "2025-10-23T17:03:16.154165+00:00", + "last_updated": "2025-10-23T17:03:16.154165+00:00", + "description": "Betty Framework Certified Agents Marketplace", + "total_agents": 5, + "certified_count": 3, + "draft_count": 2, + "catalog": [ + { + "name": "api.designer", + "version": "0.1.0", + "description": "Design RESTful APIs following enterprise guidelines", + "status": "certified", + "reasoning_mode": "iterative", + "skills_available": ["api.define", "api.validate"], + "capabilities": [ + "Design RESTful APIs from natural language requirements" + ], + "tags": ["api", "design", "openapi"], + "maintainer": "Betty Core Team", + "documentation_url": "https://betty-framework.dev/docs/agents/api.designer", + "dependencies": ["context.schema"] + } + ] +} +``` + +### Commands Marketplace Structure + +```json +{ + "marketplace_version": "1.0.0", + "generated_at": "2025-10-23T17:51:58.579847+00:00", + "last_updated": "2025-10-23T17:51:58.579847+00:00", + "description": "Betty Framework Certified Commands Marketplace", + "total_commands": 4, + "certified_count": 1, + "draft_count": 3, + "catalog": [ + { + "name": "/test-workflow-command", + "version": "1.0.0", + "description": "Test complete workflow", + "status": "certified", + "tags": ["test", "workflow"], + "execution": { + "type": "skill", + "target": "api.validate" + }, + "parameters": [ + { + "name": "input", + "type": "string", + "required": true, + "description": "Input parameter" + } + ], + "maintainer": "Betty Core Team" + } + ] +} +``` + +### Hooks Marketplace Structure + +```json +{ + "marketplace_version": "1.0.0", + "generated_at": "2025-10-23T17:51:58.579847+00:00", + "last_updated": "2025-10-23T17:51:58.579847+00:00", + "description": "Betty Framework Certified Hooks Marketplace", + "total_hooks": 4, + "certified_count": 1, + "draft_count": 3, + "catalog": [ + { + "name": "test-workflow-hook", + "version": "1.0.0", + "description": "Test complete workflow", + "status": "certified", + "tags": ["test", "workflow", "openapi"], + "event": "on_file_edit", + "command": "python validate.py {file_path}", + "blocking": true, + "when": { + "pattern": "*.openapi.yaml" + }, + "timeout": 30000, + "on_failure": "show_errors", + "maintainer": "Betty Core Team" + } + ] +} +``` + +## Marketplace Transformations + +### From Registry to Marketplace + +The skill transforms registry entries to marketplace format: + +#### Skills and Agents + +| Registry Field | Marketplace Field | Transformation | +|----------------|-------------------|----------------| +| `status: "active"` | `status: "certified"` | Renamed for marketplace context | +| `name` | `name` | Preserved | +| `version` | `version` | Preserved | +| `description` | `description` | Preserved | +| `tags` | `tags` | Preserved (default: `[]`) | +| `dependencies` | `dependencies` | Preserved (default: `[]`) | +| `entrypoints` | `entrypoints` | Preserved (skills only) | +| `inputs` | `inputs` | Preserved (skills only) | +| `outputs` | `outputs` | Preserved (skills only) | +| `skills_available` | `skills_available` | Preserved (agents only) | +| `capabilities` | `capabilities` | Preserved (agents only) | +| `reasoning_mode` | `reasoning_mode` | Preserved (agents only) | +| N/A | `maintainer` | Added (default: "Betty Core Team") | +| N/A | `usage_examples` | Generated from entrypoints or provided | +| N/A | `documentation_url` | Generated: `https://betty-framework.dev/docs/{type}/{name}` | +| N/A | `last_updated` | Added: ISO timestamp | + +#### Commands + +| Registry Field | Marketplace Field | Transformation | +|----------------|-------------------|----------------| +| `status: "active"` | `status: "certified"` | Renamed for marketplace context | +| `name` | `name` | Preserved | +| `version` | `version` | Preserved | +| `description` | `description` | Preserved | +| `tags` | `tags` | Preserved (default: `[]`) | +| `execution` | `execution` | Preserved | +| `parameters` | `parameters` | Preserved (default: `[]`) | +| N/A | `maintainer` | Added (default: "Betty Core Team") | +| N/A | `last_updated` | Added: ISO timestamp | + +#### Hooks + +| Registry Field | Marketplace Field | Transformation | +|----------------|-------------------|----------------| +| `status: "active"` | `status: "certified"` | Renamed for marketplace context | +| `name` | `name` | Preserved | +| `version` | `version` | Preserved | +| `description` | `description` | Preserved | +| `tags` | `tags` | Preserved (default: `[]`) | +| `event` | `event` | Preserved | +| `command` | `command` | Preserved | +| `blocking` | `blocking` | Preserved (default: `false`) | +| `when` | `when` | Preserved (default: `{}`) | +| `timeout` | `timeout` | Preserved | +| `on_failure` | `on_failure` | Preserved | +| N/A | `maintainer` | Added (default: "Betty Core Team") | +| N/A | `last_updated` | Added: ISO timestamp | + +### Metadata Enrichment + +The skill adds marketplace-specific metadata: + +1. **Maintainer**: Defaults to "Betty Core Team" if not specified +2. **Usage Examples**: Auto-generated from entrypoint commands if missing (skills only) +3. **Documentation URL**: Generated following the pattern `https://betty-framework.dev/docs/{skills|agents}/{name}` (skills and agents only) +4. **Last Updated**: ISO timestamp added to all marketplace files +5. **Statistics**: Adds total counts, certified counts, and draft counts + +## Behavior + +### 1. Registry Loading + +Reads JSON files from: +- `registry/skills.json` +- `registry/agents.json` +- `registry/commands.json` +- `registry/hooks.json` + +If a registry file is missing, the skill fails with an error. + +### 2. Filtering + +For each skill/agent/command/hook in the registry: +- Checks `status` field - must be `"active"` +- Checks `certified` field (if present) - must be `true` +- Skips items that don't meet criteria +- Logs which items are included/excluded + +### 3. Transformation + +Converts each certified entry: +- Copies core fields (name, version, description, tags) +- Transforms `status: "active"` → `status: "certified"` +- Adds marketplace metadata (maintainer, last_updated timestamp) +- For skills: Adds docs URL and generates usage examples if not provided +- For agents: Adds docs URL +- Preserves all technical details (entrypoints, inputs, outputs, execution, parameters, event, command, etc.) + +### 4. Statistics Calculation + +Tracks: +- **Total items**: All items in registry +- **Certified count**: Items included in marketplace +- **Draft count**: Items excluded (total - certified) + +### 5. File Writing + +Writes marketplace catalogs: +- Creates `marketplace/` directory if needed +- Formats JSON with 2-space indentation +- Preserves Unicode characters (no ASCII escaping) +- Adds generation timestamp + +## Outputs + +### Success Response + +```json +{ + "ok": true, + "status": "success", + "skills_output": "/home/user/betty/marketplace/skills.json", + "agents_output": "/home/user/betty/marketplace/agents.json", + "commands_output": "/home/user/betty/marketplace/commands.json", + "hooks_output": "/home/user/betty/marketplace/hooks.json", + "skills_certified": 16, + "skills_total": 20, + "agents_certified": 3, + "agents_total": 5, + "commands_certified": 1, + "commands_total": 4, + "hooks_certified": 1, + "hooks_total": 4 +} +``` + +### Failure Response + +```json +{ + "ok": false, + "status": "failed", + "error": "Registry file not found: /home/user/betty/registry/skills.json" +} +``` + +## Examples + +### Example 1: Basic Marketplace Generation + +**Scenario**: Generate marketplace catalogs after adding new certified skills + +```bash +# Register new skills +/skill/define skills/data.transform/skill.yaml +/skill/define skills/api.monitor/skill.yaml + +# Update registry +/registry/update + +# Generate marketplace +/marketplace/generate +``` + +**Output**: +``` +INFO: Starting marketplace catalog generation from registries... +INFO: Loading registry files... +INFO: Generating marketplace catalogs... +INFO: Added certified skill: api.validate +INFO: Added certified skill: api.define +INFO: Skipped non-certified skill: test.hello (status: draft) +INFO: Added certified agent: api.designer +INFO: Added certified command: /test-workflow-command +INFO: Skipped non-certified command: /test-command (status: draft) +INFO: Added certified hook: test-workflow-hook +INFO: Skipped non-certified hook: test-validation-hook (status: draft) +INFO: Writing marketplace files... +INFO: ✅ Written marketplace file to /home/user/betty/marketplace/skills.json +INFO: ✅ Written marketplace file to /home/user/betty/marketplace/agents.json +INFO: ✅ Written marketplace file to /home/user/betty/marketplace/commands.json +INFO: ✅ Written marketplace file to /home/user/betty/marketplace/hooks.json +INFO: ✅ Generated marketplace catalogs: +INFO: Skills: 16/20 certified +INFO: Agents: 3/5 certified +INFO: Commands: 1/4 certified +INFO: Hooks: 1/4 certified +``` + +### Example 2: After Promoting Skills to Active + +**Scenario**: Skills were marked as active and should now appear in marketplace + +```bash +# Edit registry to mark skills as active +# (Normally done via skill.define) + +# Regenerate marketplace +/marketplace/generate +``` + +**Before** (registry): +```json +{ + "name": "my.skill", + "status": "draft" +} +``` + +**After** (registry updated): +```json +{ + "name": "my.skill", + "status": "active" +} +``` + +**Marketplace** (now includes): +```json +{ + "name": "my.skill", + "status": "certified" +} +``` + +### Example 3: Publishing to GitHub Pages + +**Scenario**: Deploy marketplace catalogs to public API endpoint + +```bash +# Generate marketplace +/marketplace/generate + +# Copy to GitHub Pages directory +cp marketplace/*.json docs/api/v1/ + +# Commit and push +git add marketplace/ docs/api/v1/ +git commit -m "Update marketplace catalog" +git push +``` + +Now accessible at: +- `https://riskexec.github.io/betty/api/v1/skills.json` +- `https://riskexec.github.io/betty/api/v1/agents.json` +- `https://riskexec.github.io/betty/api/v1/commands.json` +- `https://riskexec.github.io/betty/api/v1/hooks.json` + +### Example 4: CI/CD Integration + +**Scenario**: Auto-generate marketplace on every registry change + +```yaml +# .github/workflows/marketplace.yml +name: Update Marketplace +on: + push: + paths: + - 'registry/*.json' + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Generate Marketplace + run: python skills/generate.marketplace/generate_marketplace.py + - name: Commit Changes + run: | + git config user.name "Betty Bot" + git config user.email "bot@riskexec.com" + git add marketplace/ + git commit -m "chore: update marketplace catalog" + git push +``` + +## Integration + +### With skill.define Workflow + +After registering skills, regenerate marketplace: + +```bash +/skill/define skills/new.skill/skill.yaml +/registry/update +/marketplace/generate +``` + +### With Workflows + +Include marketplace generation as a workflow step: + +```yaml +# workflows/skill_lifecycle.yaml +steps: + - skill: skill.create + args: ["new.skill", "Description"] + + - skill: skill.define + args: ["skills/new.skill/skill.yaml"] + + - skill: registry.update + args: ["skills/new.skill/skill.yaml"] + + - skill: generate.marketplace + args: [] +``` + +### With Hooks + +Auto-regenerate marketplace when registries change: + +```yaml +# .claude/hooks.yaml +- event: on_file_save + pattern: "registry/*.json" + command: python skills/generate.marketplace/generate_marketplace.py + blocking: false + description: Auto-regenerate marketplace when registry changes +``` + +## What Gets Included + +### Included in Marketplace + +- Skills/agents/commands/hooks with `status: "active"` +- Skills/agents/commands/hooks with `certified: true` (if field exists) +- All technical metadata (entrypoints, inputs, outputs, execution, parameters, event, command, etc.) +- All semantic metadata (tags, dependencies) +- Last updated timestamp for all entries + +### Not Included + +- Skills/agents/commands/hooks with `status: "draft"` +- Skills/agents/commands/hooks with `status: "deprecated"` +- Skills/agents/commands/hooks with `certified: false` +- Internal-only items +- Test/experimental items (unless marked active) + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "Registry file not found" | Missing registry file | Ensure `registry/skills.json` and `registry/agents.json` exist | +| "Failed to parse JSON" | Invalid JSON syntax | Fix JSON syntax in registry files | +| "Permission denied" | Cannot write marketplace files | Check write permissions on `marketplace/` directory | +| Empty marketplace | No active skills | Mark skills as `status: "active"` in registry | + +## Files Read + +- `registry/skills.json` - Skill registry (source) +- `registry/agents.json` - Agent registry (source) +- `registry/commands.json` - Command registry (source) +- `registry/hooks.json` - Hook registry (source) + +## Files Modified + +- `marketplace/skills.json` - Skills marketplace catalog (output) +- `marketplace/agents.json` - Agents marketplace catalog (output) +- `marketplace/commands.json` - Commands marketplace catalog (output) +- `marketplace/hooks.json` - Hooks marketplace catalog (output) + +## Exit Codes + +- **0**: Success (marketplace catalogs generated successfully) +- **1**: Failure (error during generation) + +## Logging + +Logs generation progress: + +``` +INFO: Starting marketplace catalog generation from registries... +INFO: Loading registry files... +INFO: Generating marketplace catalogs... +INFO: Added certified skill: api.validate +INFO: Added certified skill: hook.define +DEBUG: Skipped non-certified skill: test.hello (status: draft) +INFO: Added certified agent: api.designer +INFO: Added certified command: /test-workflow-command +DEBUG: Skipped non-certified command: /test-command (status: draft) +INFO: Added certified hook: test-workflow-hook +DEBUG: Skipped non-certified hook: test-validation-hook (status: draft) +INFO: Writing marketplace files... +INFO: ✅ Written marketplace file to marketplace/skills.json +INFO: ✅ Written marketplace file to marketplace/agents.json +INFO: ✅ Written marketplace file to marketplace/commands.json +INFO: ✅ Written marketplace file to marketplace/hooks.json +INFO: ✅ Generated marketplace catalogs: +INFO: Skills: 16/20 certified +INFO: Agents: 3/5 certified +INFO: Commands: 1/4 certified +INFO: Hooks: 1/4 certified +``` + +## Best Practices + +1. **Run After Registry Updates**: Regenerate marketplace after adding/updating skills +2. **Automate with CI/CD**: Set up automated marketplace generation in pipelines +3. **Review Before Publishing**: Check generated catalogs before deploying +4. **Version Control**: Commit marketplace files with registry changes +5. **Keep Registries Clean**: Remove deprecated skills to keep marketplace focused +6. **Document Thoroughly**: Ensure skills have good descriptions and examples + +## Troubleshooting + +### Marketplace Files Not Updating + +**Problem**: Changes to registry don't appear in marketplace + +**Solutions**: +- Ensure skill status is `"active"` in registry +- Check that `certified` field is `true` (if present) +- Run `/registry/update` before `/marketplace/generate` +- Verify registry JSON syntax is valid + +### Skills Missing from Marketplace + +**Problem**: Active skills not appearing in marketplace + +**Solutions**: +- Check skill status in `registry/skills.json` +- Verify no `certified: false` field +- Ensure skill.yaml has been validated with `/skill/define` +- Check logs for filtering messages + +### Empty Marketplace Catalogs + +**Problem**: Marketplace has 0 certified items + +**Solutions**: +- Mark skills as `status: "active"` in registry +- Remove `certified: false` from skill entries +- Ensure registry files are not empty +- Run `/skill/define` to register skills first + +## Version Diff (Optional) + +To add version diff vs. last release: + +```python +# Future enhancement +def get_version_diff(old_marketplace, new_marketplace): + """Compare two marketplace versions and return diff.""" + added = [s for s in new if s not in old] + removed = [s for s in old if s not in new] + updated = [s for s in new if s in old and version_changed(s)] + return {"added": added, "removed": removed, "updated": updated} +``` + +## Upload to API (Optional) + +To upload generated catalogs to an API: + +```python +# Future enhancement +import requests + +def upload_to_api(marketplace_data, api_endpoint, api_key): + """Upload marketplace catalog to internal API.""" + response = requests.post( + api_endpoint, + json=marketplace_data, + headers={"Authorization": f"Bearer {api_key}"} + ) + return response.status_code == 200 +``` + +## Architecture + +### Skill Categories + +**Infrastructure** - generate.marketplace maintains the marketplace layer by transforming registry state into certified catalogs. + +### Design Principles + +- **Single Source of Truth**: Registry files are the source +- **Idempotent**: Can be run multiple times safely +- **Certification Filter**: Only production-ready items included +- **Metadata Enrichment**: Adds marketplace-specific fields +- **Clear Statistics**: Reports certification rates + +## See Also + +- **plugin.sync** - Generate plugin.yaml from registries ([SKILL.md](../plugin.sync/SKILL.md)) +- **registry.update** - Update skill registry ([SKILL.md](../registry.update/SKILL.md)) +- **skill.define** - Validate and register skills ([SKILL.md](../skill.define/SKILL.md)) +- **Betty Architecture** - Framework overview ([betty-architecture.md](../../docs/betty-architecture.md)) + +## Dependencies + +- **registry.update**: Registry management +- **betty.config**: Configuration constants and paths +- **betty.logging_utils**: Logging infrastructure + +## Status + +**Active** - Production-ready infrastructure skill + +## Version History + +- **0.2.0** (Oct 2025) - Added support for commands and hooks, added last_updated timestamps +- **0.1.0** (Oct 2025) - Initial implementation with filtering and marketplace generation for skills and agents diff --git a/skills/generate.marketplace/__init__.py b/skills/generate.marketplace/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/generate.marketplace/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/generate.marketplace/generate_marketplace.py b/skills/generate.marketplace/generate_marketplace.py new file mode 100755 index 0000000..d6cb318 --- /dev/null +++ b/skills/generate.marketplace/generate_marketplace.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python3 +""" +generate_marketplace.py - Implementation of the generate.marketplace Skill +Generates marketplace JSON files from registry entries: +- marketplace/skills.json +- marketplace/agents.json +- marketplace/commands.json +- marketplace/hooks.json + +Filters by status: active and certified: true (if present). +Adds last_updated ISO timestamp to all marketplace files. +""" + +import os +import sys +import json +from typing import Dict, Any, List, Optional +from datetime import datetime, timezone +from pathlib import Path + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +def load_registry_file(registry_path: str) -> Dict[str, Any]: + """ + Load a JSON registry file. + + Args: + registry_path: Path to the registry JSON file + + Returns: + Parsed registry data + + Raises: + FileNotFoundError: If registry file doesn't exist + json.JSONDecodeError: If JSON is invalid + """ + try: + with open(registry_path) as f: + return json.load(f) + except FileNotFoundError: + logger.error(f"Registry file not found: {registry_path}") + raise + except json.JSONDecodeError as e: + logger.error(f"Failed to parse JSON from {registry_path}: {e}") + raise + + +def is_certified(item: Dict[str, Any]) -> bool: + """ + Check if an item is certified for marketplace inclusion. + + Args: + item: Skill or agent entry + + Returns: + True if item should be included in marketplace + """ + # Filter by status: active + if item.get("status") != "active": + return False + + # If certified field exists, check it + if "certified" in item: + return item.get("certified") is True + + # If no certified field, consider active items as certified + return True + + +def convert_skill_to_marketplace(skill: Dict[str, Any]) -> Dict[str, Any]: + """ + Convert a registry skill entry to marketplace format. + + Args: + skill: Skill entry from registry + + Returns: + Skill entry in marketplace format + """ + skill_name = skill.get("name") + marketplace_skill = { + "name": skill_name, + "version": skill.get("version"), + "description": skill.get("description"), + "status": "certified", # Transform active -> certified for marketplace + "tags": skill.get("tags", []), + "manifest_path": f"skills/{skill_name}/skill.yaml", + "maintainer": skill.get("maintainer", "Betty Core Team"), + "usage_examples": skill.get("usage_examples", []), + "documentation_url": f"https://betty-framework.dev/docs/skills/{skill_name}", + "dependencies": skill.get("dependencies", []), + "entrypoints": skill.get("entrypoints", []), + "inputs": skill.get("inputs", []), + "outputs": skill.get("outputs", []) + } + + # Generate usage examples if not present + if not marketplace_skill["usage_examples"] and marketplace_skill["entrypoints"]: + examples = [] + for entrypoint in marketplace_skill["entrypoints"]: + command = entrypoint.get("command", "") + desc = entrypoint.get("description", skill.get("description", "")) + if command: + # Create a simple example from the command + example = f"Run {skill.get('name')}: {command}" + examples.append(example.strip()) + marketplace_skill["usage_examples"] = examples[:2] # Limit to 2 examples + + return marketplace_skill + + +def convert_agent_to_marketplace(agent: Dict[str, Any]) -> Dict[str, Any]: + """ + Convert a registry agent entry to marketplace format. + + Args: + agent: Agent entry from registry + + Returns: + Agent entry in marketplace format + """ + agent_name = agent.get("name") + marketplace_agent = { + "name": agent_name, + "version": agent.get("version"), + "description": agent.get("description"), + "status": "certified", # Transform active -> certified for marketplace + "reasoning_mode": agent.get("reasoning_mode", "oneshot"), + "skills_available": agent.get("skills_available", []), + "capabilities": agent.get("capabilities", []), + "tags": agent.get("tags", []), + "manifest_path": f"agents/{agent_name}/agent.yaml", + "maintainer": agent.get("maintainer", "Betty Core Team"), + "documentation_url": f"https://betty-framework.dev/docs/agents/{agent_name}", + "dependencies": agent.get("dependencies", []) + } + + return marketplace_agent + + +def convert_command_to_marketplace(command: Dict[str, Any]) -> Dict[str, Any]: + """ + Convert a registry command entry to marketplace format. + + Args: + command: Command entry from registry + + Returns: + Command entry in marketplace format + """ + marketplace_command = { + "name": command.get("name"), + "version": command.get("version"), + "description": command.get("description"), + "status": "certified", # Transform active -> certified for marketplace + "tags": command.get("tags", []), + "execution": command.get("execution", {}), + "parameters": command.get("parameters", []), + "maintainer": command.get("maintainer", "Betty Core Team") + } + + return marketplace_command + + +def convert_hook_to_marketplace(hook: Dict[str, Any]) -> Dict[str, Any]: + """ + Convert a registry hook entry to marketplace format. + + Args: + hook: Hook entry from registry + + Returns: + Hook entry in marketplace format + """ + marketplace_hook = { + "name": hook.get("name"), + "version": hook.get("version"), + "description": hook.get("description"), + "status": "certified", # Transform active -> certified for marketplace + "tags": hook.get("tags", []), + "event": hook.get("event"), + "command": hook.get("command"), + "blocking": hook.get("blocking", False), + "when": hook.get("when", {}), + "timeout": hook.get("timeout"), + "on_failure": hook.get("on_failure"), + "maintainer": hook.get("maintainer", "Betty Core Team") + } + + return marketplace_hook + + +def generate_skills_marketplace(registry_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate marketplace skills catalog from registry. + + Args: + registry_data: Parsed skills.json from registry + + Returns: + Marketplace-formatted skills catalog + """ + skills = registry_data.get("skills", []) + + # Filter and convert active/certified skills + certified_skills = [] + for skill in skills: + if is_certified(skill): + marketplace_skill = convert_skill_to_marketplace(skill) + certified_skills.append(marketplace_skill) + logger.info(f"Added certified skill: {skill.get('name')}") + else: + logger.debug(f"Skipped non-certified skill: {skill.get('name')} (status: {skill.get('status')})") + + # Build marketplace catalog + marketplace = { + "marketplace_version": "1.0.0", + "generated_at": datetime.now(timezone.utc).isoformat(), + "last_updated": datetime.now(timezone.utc).isoformat(), + "description": "Betty Framework Certified Skills Marketplace", + "total_skills": len(skills), + "certified_count": len(certified_skills), + "draft_count": len(skills) - len(certified_skills), + "catalog": certified_skills + } + + return marketplace + + +def generate_agents_marketplace(registry_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate marketplace agents catalog from registry. + + Args: + registry_data: Parsed agents.json from registry + + Returns: + Marketplace-formatted agents catalog + """ + agents = registry_data.get("agents", []) + + # Filter and convert active/certified agents + certified_agents = [] + for agent in agents: + if is_certified(agent): + marketplace_agent = convert_agent_to_marketplace(agent) + certified_agents.append(marketplace_agent) + logger.info(f"Added certified agent: {agent.get('name')}") + else: + logger.debug(f"Skipped non-certified agent: {agent.get('name')} (status: {agent.get('status')})") + + # Build marketplace catalog + marketplace = { + "marketplace_version": "1.0.0", + "generated_at": datetime.now(timezone.utc).isoformat(), + "last_updated": datetime.now(timezone.utc).isoformat(), + "description": "Betty Framework Certified Agents Marketplace", + "total_agents": len(agents), + "certified_count": len(certified_agents), + "draft_count": len(agents) - len(certified_agents), + "catalog": certified_agents + } + + return marketplace + + +def generate_commands_marketplace(registry_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate marketplace commands catalog from registry. + + Args: + registry_data: Parsed commands.json from registry + + Returns: + Marketplace-formatted commands catalog + """ + commands = registry_data.get("commands", []) + + # Filter and convert active/certified commands + certified_commands = [] + for command in commands: + if is_certified(command): + marketplace_command = convert_command_to_marketplace(command) + certified_commands.append(marketplace_command) + logger.info(f"Added certified command: {command.get('name')}") + else: + logger.debug(f"Skipped non-certified command: {command.get('name')} (status: {command.get('status')})") + + # Build marketplace catalog + marketplace = { + "marketplace_version": "1.0.0", + "generated_at": datetime.now(timezone.utc).isoformat(), + "last_updated": datetime.now(timezone.utc).isoformat(), + "description": "Betty Framework Certified Commands Marketplace", + "total_commands": len(commands), + "certified_count": len(certified_commands), + "draft_count": len(commands) - len(certified_commands), + "catalog": certified_commands + } + + return marketplace + + +def generate_hooks_marketplace(registry_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate marketplace hooks catalog from registry. + + Args: + registry_data: Parsed hooks.json from registry + + Returns: + Marketplace-formatted hooks catalog + """ + hooks = registry_data.get("hooks", []) + + # Filter and convert active/certified hooks + certified_hooks = [] + for hook in hooks: + if is_certified(hook): + marketplace_hook = convert_hook_to_marketplace(hook) + certified_hooks.append(marketplace_hook) + logger.info(f"Added certified hook: {hook.get('name')}") + else: + logger.debug(f"Skipped non-certified hook: {hook.get('name')} (status: {hook.get('status')})") + + # Build marketplace catalog + marketplace = { + "marketplace_version": "1.0.0", + "generated_at": datetime.now(timezone.utc).isoformat(), + "last_updated": datetime.now(timezone.utc).isoformat(), + "description": "Betty Framework Certified Hooks Marketplace", + "total_hooks": len(hooks), + "certified_count": len(certified_hooks), + "draft_count": len(hooks) - len(certified_hooks), + "catalog": certified_hooks + } + + return marketplace + + +def write_marketplace_file(data: Dict[str, Any], output_path: str): + """ + Write marketplace JSON file with proper formatting. + + Args: + data: Marketplace data dictionary + output_path: Path where to write the file + """ + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + with open(output_path, 'w') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + logger.info(f"✅ Written marketplace file to {output_path}") + + +def generate_claude_code_marketplace() -> Dict[str, Any]: + """ + Generate Claude Code marketplace.json file. + + This file lists Betty Framework as an installable plugin in Claude Code's + marketplace format, as specified in Claude Code's plugin marketplace documentation. + + Returns: + Claude Code marketplace JSON structure + """ + # Load plugin.yaml to get current metadata + plugin_yaml_path = os.path.join(BASE_DIR, "plugin.yaml") + + try: + import yaml + with open(plugin_yaml_path, 'r') as f: + plugin_data = yaml.safe_load(f) + except Exception as e: + logger.warning(f"Could not load plugin.yaml: {e}. Using defaults.") + plugin_data = {} + + # Build Claude Code marketplace structure + marketplace = { + "name": "betty-marketplace", + "version": plugin_data.get("version", "1.0.0"), + "description": "Betty Framework Plugin Marketplace - Enterprise-grade AI-assisted engineering framework", + "owner": { + "name": plugin_data.get("author", {}).get("name", "RiskExec"), + "email": plugin_data.get("author", {}).get("email", "platform@riskexec.com"), + "url": plugin_data.get("author", {}).get("url", "https://github.com/epieczko/betty") + }, + "metadata": { + "homepage": plugin_data.get("metadata", {}).get("homepage", "https://github.com/epieczko/betty"), + "repository": plugin_data.get("metadata", {}).get("repository", "https://github.com/epieczko/betty"), + "documentation": plugin_data.get("metadata", {}).get("documentation", "https://github.com/epieczko/betty/tree/main/docs"), + "generated_at": datetime.now(timezone.utc).isoformat(), + "generated_by": "generate.marketplace skill" + }, + "plugins": [ + { + "name": plugin_data.get("name", "betty-framework"), + "source": ".", + "version": plugin_data.get("version", "1.0.0"), + "description": plugin_data.get("description", "Betty Framework for structured AI-assisted engineering"), + "author": { + "name": plugin_data.get("author", {}).get("name", "RiskExec"), + "email": plugin_data.get("author", {}).get("email", "platform@riskexec.com"), + "url": plugin_data.get("author", {}).get("url", "https://github.com/epieczko/betty") + }, + "license": plugin_data.get("license", "MIT"), + "tags": plugin_data.get("metadata", {}).get("tags", [ + "framework", + "api-development", + "workflow", + "governance", + "enterprise" + ]), + "requirements": plugin_data.get("requirements", { + "python": ">=3.11", + "packages": ["pyyaml"] + }), + "strict": True # Requires plugin.yaml manifest + } + ] + } + + return marketplace + + +def main(): + """Main CLI entry point.""" + logger.info("Starting marketplace catalog generation from registries...") + + # Define registry and output paths + skills_registry_path = os.path.join(BASE_DIR, "registry", "skills.json") + agents_registry_path = os.path.join(BASE_DIR, "registry", "agents.json") + commands_registry_path = os.path.join(BASE_DIR, "registry", "commands.json") + hooks_registry_path = os.path.join(BASE_DIR, "registry", "hooks.json") + + marketplace_dir = os.path.join(BASE_DIR, "marketplace") + skills_output_path = os.path.join(marketplace_dir, "skills.json") + agents_output_path = os.path.join(marketplace_dir, "agents.json") + commands_output_path = os.path.join(marketplace_dir, "commands.json") + hooks_output_path = os.path.join(marketplace_dir, "hooks.json") + + try: + # Load registry files + logger.info("Loading registry files...") + skills_registry = load_registry_file(skills_registry_path) + agents_registry = load_registry_file(agents_registry_path) + commands_registry = load_registry_file(commands_registry_path) + hooks_registry = load_registry_file(hooks_registry_path) + + # Generate marketplace catalogs + logger.info("Generating marketplace catalogs...") + skills_marketplace = generate_skills_marketplace(skills_registry) + agents_marketplace = generate_agents_marketplace(agents_registry) + commands_marketplace = generate_commands_marketplace(commands_registry) + hooks_marketplace = generate_hooks_marketplace(hooks_registry) + + # Write Betty marketplace catalogs to files + logger.info("Writing Betty marketplace files...") + write_marketplace_file(skills_marketplace, skills_output_path) + write_marketplace_file(agents_marketplace, agents_output_path) + write_marketplace_file(commands_marketplace, commands_output_path) + write_marketplace_file(hooks_marketplace, hooks_output_path) + + # Generate and write Claude Code marketplace.json + logger.info("Generating Claude Code marketplace.json...") + claude_marketplace = generate_claude_code_marketplace() + claude_marketplace_path = os.path.join(BASE_DIR, ".claude-plugin", "marketplace.json") + write_marketplace_file(claude_marketplace, claude_marketplace_path) + + # Report results + result = { + "ok": True, + "status": "success", + "skills_output": skills_output_path, + "agents_output": agents_output_path, + "commands_output": commands_output_path, + "hooks_output": hooks_output_path, + "claude_marketplace_output": claude_marketplace_path, + "skills_certified": skills_marketplace["certified_count"], + "skills_total": skills_marketplace["total_skills"], + "agents_certified": agents_marketplace["certified_count"], + "agents_total": agents_marketplace["total_agents"], + "commands_certified": commands_marketplace["certified_count"], + "commands_total": commands_marketplace["total_commands"], + "hooks_certified": hooks_marketplace["certified_count"], + "hooks_total": hooks_marketplace["total_hooks"] + } + + # Print summary + logger.info(f"✅ Generated marketplace catalogs:") + logger.info(f" Skills: {result['skills_certified']}/{result['skills_total']} certified") + logger.info(f" Agents: {result['agents_certified']}/{result['agents_total']} certified") + logger.info(f" Commands: {result['commands_certified']}/{result['commands_total']} certified") + logger.info(f" Hooks: {result['hooks_certified']}/{result['hooks_total']} certified") + logger.info(f"📄 Outputs:") + logger.info(f" - {skills_output_path}") + logger.info(f" - {agents_output_path}") + logger.info(f" - {commands_output_path}") + logger.info(f" - {hooks_output_path}") + logger.info(f" - {claude_marketplace_path} (Claude Code format)") + + print(json.dumps(result, indent=2)) + sys.exit(0) + + except Exception as e: + logger.error(f"Failed to generate marketplace catalogs: {e}") + result = { + "ok": False, + "status": "failed", + "error": str(e) + } + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/generate.marketplace/skill.yaml b/skills/generate.marketplace/skill.yaml new file mode 100644 index 0000000..5d0c078 --- /dev/null +++ b/skills/generate.marketplace/skill.yaml @@ -0,0 +1,37 @@ +name: generate.marketplace +version: 0.3.0 +description: > + Generate marketplace catalog files from Betty Framework registries. + Filters active and certified skills/agents/commands/hooks and outputs marketplace-ready JSON files + with last_updated timestamps. Also generates Claude Code marketplace.json format. +inputs: [] +outputs: + - marketplace/skills.json + - marketplace/agents.json + - marketplace/commands.json + - marketplace/hooks.json + - .claude-plugin/marketplace.json +dependencies: + - registry.update +status: active + +entrypoints: + - command: /marketplace/generate + handler: generate_marketplace.py + runtime: python + description: > + Generate marketplace catalogs from registry files. Filters by status: active and certified: true. + Outputs skills, agents, commands, and hooks marketplace files with last_updated timestamps. + Also generates .claude-plugin/marketplace.json in Claude Code marketplace format. + parameters: [] + permissions: + - filesystem:read + - filesystem:write + +tags: + - marketplace + - registry + - automation + - infrastructure + - commands + - hooks diff --git a/skills/git.cleanupbranches/README.md b/skills/git.cleanupbranches/README.md new file mode 100644 index 0000000..42ab302 --- /dev/null +++ b/skills/git.cleanupbranches/README.md @@ -0,0 +1,50 @@ +# git.cleanupbranches + +Clean up merged and stale git branches both locally and remotely. Analyzes branch status, identifies branches that are safe to delete (merged or stale), and provides interactive cleanup with safety checks. - git-repository - Local git repository with branch information - branch-metadata - Branch merge status and last commit dates - branch-cleanup-report - Report of branches analyzed and deleted - cleanup-summary - Summary with statistics (branches deleted, kept, errors) - dry_run (boolean): Show what would be deleted without deleting (default: true) - include_remote (boolean): Also clean up remote branches (default: false) - stale_days (integer): Consider branches stale after N days of no commits (default: 30) - protected_branches (array): Branches to never delete (default: ["main", "master", "develop", "development"]) - interactive (boolean): Ask for confirmation before deleting (default: true) - merged_only (boolean): Only delete merged branches, ignore stale (default: false) - git command line tool - Access to git repository (read for analysis, write for deletion) - Access to remote repository (if include_remote=true) 1. Validate we're in a git repository 2. Get list of all local branches 3. Identify current branch (never delete) 4. For each branch: - Check if in protected list - Check if merged into main/master/develop - Check last commit date for staleness - Calculate deletion recommendation 5. Build list of branches to delete (merged or stale) 6. Display analysis results to user 7. If interactive, ask for confirmation 8. If confirmed (or not interactive): - Delete local branches - If include_remote, delete from remote - Track successes and failures 9. Generate cleanup report with statistics 10. Return structured results - Never deletes current branch - Never deletes protected branches (main, master, develop) - Default is dry_run=true (shows what would happen) - Interactive confirmation by default - Detailed logging of all operations - Rollback information provided ```python python3 skills/git.cleanupbranches/git_cleanupbranches.py --dry-run python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run --stale-days 60 python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run --include-remote python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run --no-interactive --merged-only ``` ```json { "status": "success", "analyzed": 25, "deleted": 5, "kept": 20, "branches_deleted": ["feature/old-feature", "fix/old-bug"], "branches_kept": ["feature/active", "main", "develop"], "protected": 3, "dry_run": false, "errors": [] } ``` - git - cleanup - maintenance - branches - automation This skill requires SKILL_AND_COMMAND pattern due to: - 8-10 steps (exceeds threshold) - Medium autonomy (analyzes and recommends deletions) - Reusable for CI/CD and release workflows - Complex logic with safety checks and interactive confirmation + +## Overview + +**Purpose:** Clean up merged and stale git branches both locally and remotely. Analyzes branch status, identifies branches that are safe to delete (merged or stale), and provides interactive cleanup with safety checks. - git-repository - Local git repository with branch information - branch-metadata - Branch merge status and last commit dates - branch-cleanup-report - Report of branches analyzed and deleted - cleanup-summary - Summary with statistics (branches deleted, kept, errors) - dry_run (boolean): Show what would be deleted without deleting (default: true) - include_remote (boolean): Also clean up remote branches (default: false) - stale_days (integer): Consider branches stale after N days of no commits (default: 30) - protected_branches (array): Branches to never delete (default: ["main", "master", "develop", "development"]) - interactive (boolean): Ask for confirmation before deleting (default: true) - merged_only (boolean): Only delete merged branches, ignore stale (default: false) - git command line tool - Access to git repository (read for analysis, write for deletion) - Access to remote repository (if include_remote=true) 1. Validate we're in a git repository 2. Get list of all local branches 3. Identify current branch (never delete) 4. For each branch: - Check if in protected list - Check if merged into main/master/develop - Check last commit date for staleness - Calculate deletion recommendation 5. Build list of branches to delete (merged or stale) 6. Display analysis results to user 7. If interactive, ask for confirmation 8. If confirmed (or not interactive): - Delete local branches - If include_remote, delete from remote - Track successes and failures 9. Generate cleanup report with statistics 10. Return structured results - Never deletes current branch - Never deletes protected branches (main, master, develop) - Default is dry_run=true (shows what would happen) - Interactive confirmation by default - Detailed logging of all operations - Rollback information provided ```python python3 skills/git.cleanupbranches/git_cleanupbranches.py --dry-run python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run --stale-days 60 python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run --include-remote python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run --no-interactive --merged-only ``` ```json { "status": "success", "analyzed": 25, "deleted": 5, "kept": 20, "branches_deleted": ["feature/old-feature", "fix/old-bug"], "branches_kept": ["feature/active", "main", "develop"], "protected": 3, "dry_run": false, "errors": [] } ``` - git - cleanup - maintenance - branches - automation This skill requires SKILL_AND_COMMAND pattern due to: - 8-10 steps (exceeds threshold) - Medium autonomy (analyzes and recommends deletions) - Reusable for CI/CD and release workflows - Complex logic with safety checks and interactive confirmation + +**Command:** `/git/cleanupbranches` + +## Usage + +### Basic Usage + +```bash +python3 skills/git/cleanupbranches/git_cleanupbranches.py +``` + +### With Arguments + +```bash +python3 skills/git/cleanupbranches/git_cleanupbranches.py \ + --output-format json +``` + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - git.cleanupbranches +``` + +## Testing + +Run tests with: + +```bash +pytest skills/git/cleanupbranches/test_git_cleanupbranches.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/git.cleanupbranches/__init__.py b/skills/git.cleanupbranches/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/git.cleanupbranches/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/git.cleanupbranches/git_cleanupbranches.py b/skills/git.cleanupbranches/git_cleanupbranches.py new file mode 100755 index 0000000..80c6a4d --- /dev/null +++ b/skills/git.cleanupbranches/git_cleanupbranches.py @@ -0,0 +1,472 @@ +#!/usr/bin/env python3 +""" +git.cleanupbranches - Clean up merged and stale git branches + +Analyzes branch status, identifies branches that are safe to delete (merged or stale), +and provides interactive cleanup with safety checks. + +Generated by meta.skill with Betty Framework certification +""" + +import os +import sys +import json +import yaml +import subprocess +from pathlib import Path +from typing import Dict, List, Any, Optional +from datetime import datetime, timedelta + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.certification import certified_skill + +logger = setup_logger(__name__) + + +class GitCleanupbranches: + """ + Clean up merged and stale git branches both locally and remotely. + """ + + def __init__(self, base_dir: str = "."): + """Initialize skill""" + self.base_dir = Path(base_dir) + self.protected_branches = ["main", "master", "develop", "development"] + + def run_git_command(self, command: List[str]) -> tuple[bool, str]: + """ + Run a git command and return success status and output + + Args: + command: Git command as list of arguments + + Returns: + Tuple of (success, output) + """ + try: + result = subprocess.run( + command, + cwd=self.base_dir, + capture_output=True, + text=True, + check=True + ) + return True, result.stdout.strip() + except subprocess.CalledProcessError as e: + return False, e.stderr.strip() + + def is_git_repository(self) -> bool: + """Check if current directory is a git repository""" + success, _ = self.run_git_command(["git", "rev-parse", "--is-inside-work-tree"]) + return success + + def get_current_branch(self) -> Optional[str]: + """Get the current branch name""" + success, output = self.run_git_command(["git", "branch", "--show-current"]) + return output if success else None + + def get_all_local_branches(self) -> List[str]: + """Get list of all local branches""" + success, output = self.run_git_command(["git", "branch", "--format=%(refname:short)"]) + if not success: + return [] + return [b.strip() for b in output.split('\n') if b.strip()] + + def is_branch_merged(self, branch: str, into_branch: str = "main") -> bool: + """ + Check if a branch is merged into another branch + + Args: + branch: Branch to check + into_branch: Branch to check against + + Returns: + True if merged, False otherwise + """ + # Try multiple base branches + for base in ["main", "master", "develop"]: + success, output = self.run_git_command( + ["git", "branch", "--merged", base, "--format=%(refname:short)"] + ) + if success and branch in output.split('\n'): + return True + return False + + def get_branch_last_commit_date(self, branch: str) -> Optional[datetime]: + """ + Get the date of the last commit on a branch + + Args: + branch: Branch name + + Returns: + Datetime of last commit or None + """ + success, output = self.run_git_command( + ["git", "log", "-1", "--format=%ct", branch] + ) + if success and output: + try: + timestamp = int(output) + return datetime.fromtimestamp(timestamp) + except (ValueError, OSError): + return None + return None + + def is_branch_stale(self, branch: str, days: int = 30) -> bool: + """ + Check if a branch is stale (no commits for N days) + + Args: + branch: Branch name + days: Number of days to consider stale + + Returns: + True if stale, False otherwise + """ + last_commit = self.get_branch_last_commit_date(branch) + if last_commit is None: + return False + + cutoff_date = datetime.now() - timedelta(days=days) + return last_commit < cutoff_date + + def delete_local_branch(self, branch: str, force: bool = False) -> bool: + """ + Delete a local branch + + Args: + branch: Branch name + force: Use -D instead of -d + + Returns: + True if deleted successfully + """ + flag = "-D" if force else "-d" + success, output = self.run_git_command(["git", "branch", flag, branch]) + if success: + logger.info(f"Deleted local branch: {branch}") + else: + logger.warning(f"Failed to delete branch {branch}: {output}") + return success + + def delete_remote_branch(self, branch: str) -> bool: + """ + Delete a remote branch + + Args: + branch: Branch name + + Returns: + True if deleted successfully + """ + success, output = self.run_git_command(["git", "push", "origin", "--delete", branch]) + if success: + logger.info(f"Deleted remote branch: {branch}") + else: + logger.warning(f"Failed to delete remote branch {branch}: {output}") + return success + + @certified_skill("git.cleanupbranches") + def execute( + self, + dry_run: bool = True, + include_remote: bool = False, + stale_days: int = 30, + protected_branches: Optional[List[str]] = None, + interactive: bool = True, + merged_only: bool = False + ) -> Dict[str, Any]: + """ + Execute the skill + + Args: + dry_run: Show what would be deleted without deleting + include_remote: Also clean up remote branches + stale_days: Consider branches stale after N days + protected_branches: Branches to never delete + interactive: Ask for confirmation before deleting + merged_only: Only delete merged branches + + Returns: + Dict with execution results + """ + try: + logger.info("Executing git.cleanupbranches...") + + # Validate we're in a git repository + if not self.is_git_repository(): + return { + "ok": False, + "status": "failed", + "error": "Not in a git repository" + } + + # Use provided protected branches or defaults + if protected_branches: + self.protected_branches = protected_branches + + # Get current branch (never delete) + current_branch = self.get_current_branch() + if not current_branch: + return { + "ok": False, + "status": "failed", + "error": "Could not determine current branch" + } + + # Get all local branches + all_branches = self.get_all_local_branches() + logger.info(f"Found {len(all_branches)} local branches") + + # Analyze branches + branches_to_delete = [] + branches_kept = [] + analysis = [] + + for branch in all_branches: + # Skip current branch + if branch == current_branch: + branches_kept.append(branch) + analysis.append({ + "branch": branch, + "action": "keep", + "reason": "current branch" + }) + continue + + # Skip protected branches + if branch in self.protected_branches: + branches_kept.append(branch) + analysis.append({ + "branch": branch, + "action": "keep", + "reason": "protected" + }) + continue + + # Check if merged + is_merged = self.is_branch_merged(branch) + + # Check if stale + is_stale = False if merged_only else self.is_branch_stale(branch, stale_days) + + # Determine if should delete + should_delete = is_merged or (is_stale and not merged_only) + + if should_delete: + reason = "merged" if is_merged else f"stale ({stale_days}+ days)" + branches_to_delete.append(branch) + analysis.append({ + "branch": branch, + "action": "delete", + "reason": reason, + "is_merged": is_merged, + "is_stale": is_stale + }) + else: + branches_kept.append(branch) + analysis.append({ + "branch": branch, + "action": "keep", + "reason": "active" + }) + + # Display analysis + logger.info(f"Analysis complete:") + logger.info(f" Total branches: {len(all_branches)}") + logger.info(f" To delete: {len(branches_to_delete)}") + logger.info(f" To keep: {len(branches_kept)}") + + if dry_run: + logger.info("DRY RUN - No branches will be deleted") + logger.info("Branches that would be deleted:") + for item in analysis: + if item["action"] == "delete": + logger.info(f" - {item['branch']} ({item['reason']})") + else: + # Interactive confirmation + if interactive and branches_to_delete: + logger.info("Branches to delete:") + for item in analysis: + if item["action"] == "delete": + logger.info(f" - {item['branch']} ({item['reason']})") + + response = input("\nProceed with deletion? (yes/no): ").strip().lower() + if response not in ["yes", "y"]: + logger.info("Aborted by user") + return { + "ok": True, + "status": "aborted", + "message": "Aborted by user", + "analyzed": len(all_branches), + "would_delete": len(branches_to_delete) + } + + # Delete branches + deleted = [] + errors = [] + + for branch in branches_to_delete: + # Delete local + if self.delete_local_branch(branch): + deleted.append(branch) + + # Delete remote if requested + if include_remote: + self.delete_remote_branch(branch) + else: + errors.append(f"Failed to delete {branch}") + + logger.info(f"Deleted {len(deleted)} branches") + if errors: + logger.warning(f"{len(errors)} errors occurred") + + # Build result + result = { + "ok": True, + "status": "success", + "analyzed": len(all_branches), + "deleted": len(branches_to_delete) if not dry_run else 0, + "kept": len(branches_kept), + "branches_to_delete": branches_to_delete, + "branches_kept": branches_kept, + "protected": len([b for b in all_branches if b in self.protected_branches]), + "dry_run": dry_run, + "analysis": analysis + } + + if not dry_run and branches_to_delete: + result["branches_deleted"] = deleted if not dry_run else [] + result["errors"] = errors if not dry_run else [] + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Clean up merged and stale git branches" + ) + + parser.add_argument( + "--dry-run", + action="store_true", + default=True, + help="Show what would be deleted without deleting (default: true)" + ) + + parser.add_argument( + "--no-dry-run", + dest="dry_run", + action="store_false", + help="Actually delete branches" + ) + + parser.add_argument( + "--include-remote", + action="store_true", + default=False, + help="Also delete remote branches" + ) + + parser.add_argument( + "--stale-days", + type=int, + default=30, + help="Consider branches stale after N days (default: 30)" + ) + + parser.add_argument( + "--protected-branches", + nargs="+", + default=["main", "master", "develop", "development"], + help="Branches to never delete" + ) + + parser.add_argument( + "--interactive", + action="store_true", + default=True, + help="Ask for confirmation before deleting (default: true)" + ) + + parser.add_argument( + "--no-interactive", + dest="interactive", + action="store_false", + help="Don't ask for confirmation" + ) + + parser.add_argument( + "--merged-only", + action="store_true", + default=False, + help="Only delete merged branches, ignore stale" + ) + + parser.add_argument( + "--output-format", + choices=["json", "yaml", "human"], + default="human", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = GitCleanupbranches() + + # Execute skill + result = skill.execute( + dry_run=args.dry_run, + include_remote=args.include_remote, + stale_days=args.stale_days, + protected_branches=args.protected_branches, + interactive=args.interactive, + merged_only=args.merged_only + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + elif args.output_format == "yaml": + print(yaml.dump(result, default_flow_style=False)) + else: + # Human-readable output + if result.get("ok"): + print(f"\n✓ Branch Cleanup {'(DRY RUN)' if result.get('dry_run') else ''}") + print(f" Analyzed: {result.get('analyzed', 0)} branches") + if result.get('dry_run'): + print(f" Would delete: {len(result.get('branches_to_delete', []))} branches") + else: + print(f" Deleted: {result.get('deleted', 0)} branches") + print(f" Kept: {result.get('kept', 0)} branches") + print(f" Protected: {result.get('protected', 0)} branches") + + if result.get('branches_to_delete'): + print(f"\nBranches to delete:") + for branch in result.get('branches_to_delete', []): + print(f" - {branch}") + else: + print(f"\n✗ Error: {result.get('error', 'Unknown error')}") + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/git.cleanupbranches/skill.yaml b/skills/git.cleanupbranches/skill.yaml new file mode 100644 index 0000000..d61ab77 --- /dev/null +++ b/skills/git.cleanupbranches/skill.yaml @@ -0,0 +1,47 @@ +name: git.cleanupbranches +version: 0.1.0 +description: 'Clean up merged and stale git branches both locally and remotely. Analyzes + branch status, identifies branches that are safe to delete (merged or stale), and + provides interactive cleanup with safety checks. - git-repository - Local git repository + with branch information - branch-metadata - Branch merge status and last commit + dates - branch-cleanup-report - Report of branches analyzed and deleted - cleanup-summary + - Summary with statistics (branches deleted, kept, errors) - dry_run (boolean): + Show what would be deleted without deleting (default: true) - include_remote (boolean): + Also clean up remote branches (default: false) - stale_days (integer): Consider + branches stale after N days of no commits (default: 30) - protected_branches (array): + Branches to never delete (default: ["main", "master", "develop", "development"]) + - interactive (boolean): Ask for confirmation before deleting (default: true) - + merged_only (boolean): Only delete merged branches, ignore stale (default: false) + - git command line tool - Access to git repository (read for analysis, write for + deletion) - Access to remote repository (if include_remote=true) 1. Validate we''re + in a git repository 2. Get list of all local branches 3. Identify current branch + (never delete) 4. For each branch: - Check if in protected list - Check if merged + into main/master/develop - Check last commit date for staleness - Calculate deletion + recommendation 5. Build list of branches to delete (merged or stale) 6. Display + analysis results to user 7. If interactive, ask for confirmation 8. If confirmed + (or not interactive): - Delete local branches - If include_remote, delete from remote + - Track successes and failures 9. Generate cleanup report with statistics 10. Return + structured results - Never deletes current branch - Never deletes protected branches + (main, master, develop) - Default is dry_run=true (shows what would happen) - Interactive + confirmation by default - Detailed logging of all operations - Rollback information + provided ```python python3 skills/git.cleanupbranches/git_cleanupbranches.py --dry-run + python3 skills/git.cleanupbranches/git_cleanupbranches.py --no-dry-run python3 skills/git.cleanupbranches/git_cleanupbranches.py + --no-dry-run --stale-days 60 python3 skills/git.cleanupbranches/git_cleanupbranches.py + --no-dry-run --include-remote python3 skills/git.cleanupbranches/git_cleanupbranches.py + --no-dry-run --no-interactive --merged-only ``` ```json { "status": "success", "analyzed": + 25, "deleted": 5, "kept": 20, "branches_deleted": ["feature/old-feature", "fix/old-bug"], + "branches_kept": ["feature/active", "main", "develop"], "protected": 3, "dry_run": + false, "errors": [] } ``` - git - cleanup - maintenance - branches - automation + This skill requires SKILL_AND_COMMAND pattern due to: - 8-10 steps (exceeds threshold) + - Medium autonomy (analyzes and recommends deletions) - Reusable for CI/CD and release + workflows - Complex logic with safety checks and interactive confirmation' +inputs: [] +outputs: [] +status: active +permissions: [] +entrypoints: +- command: /git/cleanupbranches + handler: git_cleanupbranches.py + runtime: python + description: Clean up merged and stale git branches both locally and remotely. Analyzes + branch status, identifies diff --git a/skills/git.cleanupbranches/test_git_cleanupbranches.py b/skills/git.cleanupbranches/test_git_cleanupbranches.py new file mode 100644 index 0000000..30b0527 --- /dev/null +++ b/skills/git.cleanupbranches/test_git_cleanupbranches.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for git.cleanupbranches + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.git_cleanupbranches import git_cleanupbranches + + +class TestGitCleanupbranches: + """Tests for GitCleanupbranches""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = git_cleanupbranches.GitCleanupbranches() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["git_cleanupbranches.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + git_cleanupbranches.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Clean up merged and stale git branches both locall" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/git.createpr/README.md b/skills/git.createpr/README.md new file mode 100644 index 0000000..90efaca --- /dev/null +++ b/skills/git.createpr/README.md @@ -0,0 +1,50 @@ +# git.createpr + +Create GitHub pull requests with auto-generated titles and descriptions based on commit analysis. Analyzes commit history, identifies related issues, and creates well-formatted PRs with proper linking and metadata. - git-commits - Commit history between base and feature branch - git-repository - Local git repository with commit information - github-credentials - GitHub token for API access (from gh CLI or environment) - pull-request - Created GitHub pull request with metadata - pr-report - Summary of PR creation including URL, number, and status - base_branch (string): Base branch for PR (default: main) - title (string): PR title (optional, auto-generated from commits if not provided) - draft (boolean): Create as draft PR (default: false) - auto_merge (boolean): Enable auto-merge if checks pass (default: false) - reviewers (array): List of GitHub usernames to request reviews from - labels (array): Labels to apply to PR (optional, auto-detected from commits) - body (string): PR description (optional, auto-generated if not provided) - git command line tool - GitHub CLI (gh) or GitHub API access with token - Access to git repository - GitHub repository with permissions to create PRs 1. Validate we're in a git repository 2. Get current branch name 3. Validate base branch exists 4. Fetch latest changes from remote 5. Get commit history between base and current branch 6. Analyze commits to extract: - Commit messages - Conventional commit types (feat, fix, docs, etc.) - Issue references (#123) - Breaking changes 7. Generate PR title (if not provided): - Use most recent commit message - Or summarize multiple commits - Format: "type(scope): description" 8. Generate PR description (if not provided): - Summary of changes - List of commits with links - Related issues section - Breaking changes warning (if any) 9. Detect labels from commit types: - feat → enhancement - fix → bug - docs → documentation - etc. 10. Create PR using GitHub CLI (gh pr create): - Set title and body - Set base and head branches - Apply labels - Request reviewers - Set draft status 11. Parse PR URL and number from output 12. Return structured result with PR metadata ```bash python3 skills/git.createpr/git_createpr.py python3 skills/git.createpr/git_createpr.py --draft python3 skills/git.createpr/git_createpr.py --reviewers alice bob python3 skills/git.createpr/git_createpr.py --base develop python3 skills/git.createpr/git_createpr.py --title "feat: add user authentication" python3 skills/git.createpr/git_createpr.py --labels enhancement breaking-change ``` ```json { "ok": true, "status": "success", "pr_number": 123, "pr_url": "https://github.com/owner/repo/pull/123", "title": "feat: add user authentication", "base_branch": "main", "head_branch": "feature/auth", "commits_analyzed": 5, "issues_linked": ["#45", "#67"], "labels_applied": ["enhancement", "feature"], "reviewers_requested": ["alice", "bob"], "is_draft": false } ``` - git - github - pull-request - automation - workflow - pr This skill requires SKILL_AND_COMMAND pattern due to: - 12 steps (exceeds threshold) - High autonomy (auto-generates PR content intelligently) - Highly reusable for release automation and CI/CD - Complex GitHub API interaction - Commit analysis and pattern detection - Multiple execution contexts (CLI, agents, workflows) This implementation uses GitHub CLI (gh) for simplicity and authentication: - Leverages existing gh authentication - Simpler than managing GitHub API tokens - Better error messages - Handles pagination automatically If gh CLI is not available, falls back to REST API with token from: - GITHUB_TOKEN environment variable - GH_TOKEN environment variable + +## Overview + +**Purpose:** Create GitHub pull requests with auto-generated titles and descriptions based on commit analysis. Analyzes commit history, identifies related issues, and creates well-formatted PRs with proper linking and metadata. - git-commits - Commit history between base and feature branch - git-repository - Local git repository with commit information - github-credentials - GitHub token for API access (from gh CLI or environment) - pull-request - Created GitHub pull request with metadata - pr-report - Summary of PR creation including URL, number, and status - base_branch (string): Base branch for PR (default: main) - title (string): PR title (optional, auto-generated from commits if not provided) - draft (boolean): Create as draft PR (default: false) - auto_merge (boolean): Enable auto-merge if checks pass (default: false) - reviewers (array): List of GitHub usernames to request reviews from - labels (array): Labels to apply to PR (optional, auto-detected from commits) - body (string): PR description (optional, auto-generated if not provided) - git command line tool - GitHub CLI (gh) or GitHub API access with token - Access to git repository - GitHub repository with permissions to create PRs 1. Validate we're in a git repository 2. Get current branch name 3. Validate base branch exists 4. Fetch latest changes from remote 5. Get commit history between base and current branch 6. Analyze commits to extract: - Commit messages - Conventional commit types (feat, fix, docs, etc.) - Issue references (#123) - Breaking changes 7. Generate PR title (if not provided): - Use most recent commit message - Or summarize multiple commits - Format: "type(scope): description" 8. Generate PR description (if not provided): - Summary of changes - List of commits with links - Related issues section - Breaking changes warning (if any) 9. Detect labels from commit types: - feat → enhancement - fix → bug - docs → documentation - etc. 10. Create PR using GitHub CLI (gh pr create): - Set title and body - Set base and head branches - Apply labels - Request reviewers - Set draft status 11. Parse PR URL and number from output 12. Return structured result with PR metadata ```bash python3 skills/git.createpr/git_createpr.py python3 skills/git.createpr/git_createpr.py --draft python3 skills/git.createpr/git_createpr.py --reviewers alice bob python3 skills/git.createpr/git_createpr.py --base develop python3 skills/git.createpr/git_createpr.py --title "feat: add user authentication" python3 skills/git.createpr/git_createpr.py --labels enhancement breaking-change ``` ```json { "ok": true, "status": "success", "pr_number": 123, "pr_url": "https://github.com/owner/repo/pull/123", "title": "feat: add user authentication", "base_branch": "main", "head_branch": "feature/auth", "commits_analyzed": 5, "issues_linked": ["#45", "#67"], "labels_applied": ["enhancement", "feature"], "reviewers_requested": ["alice", "bob"], "is_draft": false } ``` - git - github - pull-request - automation - workflow - pr This skill requires SKILL_AND_COMMAND pattern due to: - 12 steps (exceeds threshold) - High autonomy (auto-generates PR content intelligently) - Highly reusable for release automation and CI/CD - Complex GitHub API interaction - Commit analysis and pattern detection - Multiple execution contexts (CLI, agents, workflows) This implementation uses GitHub CLI (gh) for simplicity and authentication: - Leverages existing gh authentication - Simpler than managing GitHub API tokens - Better error messages - Handles pagination automatically If gh CLI is not available, falls back to REST API with token from: - GITHUB_TOKEN environment variable - GH_TOKEN environment variable + +**Command:** `/git/createpr` + +## Usage + +### Basic Usage + +```bash +python3 skills/git/createpr/git_createpr.py +``` + +### With Arguments + +```bash +python3 skills/git/createpr/git_createpr.py \ + --output-format json +``` + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - git.createpr +``` + +## Testing + +Run tests with: + +```bash +pytest skills/git/createpr/test_git_createpr.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/git.createpr/__init__.py b/skills/git.createpr/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/git.createpr/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/git.createpr/git_createpr.py b/skills/git.createpr/git_createpr.py new file mode 100755 index 0000000..251c45a --- /dev/null +++ b/skills/git.createpr/git_createpr.py @@ -0,0 +1,534 @@ +#!/usr/bin/env python3 +""" +git.createpr - Create GitHub pull requests with auto-generated content + +Analyzes commit history, identifies related issues, and creates well-formatted PRs +with proper linking and metadata. + +Generated by meta.skill with Betty Framework certification +""" + +import os +import sys +import json +import yaml +import subprocess +import re +from pathlib import Path +from typing import Dict, List, Any, Optional, Tuple + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.certification import certified_skill + +logger = setup_logger(__name__) + + +class GitCreatepr: + """ + Create GitHub pull requests with auto-generated titles and descriptions. + """ + + # Conventional commit types and their labels + COMMIT_TYPE_LABELS = { + "feat": "enhancement", + "fix": "bug", + "docs": "documentation", + "style": "style", + "refactor": "refactor", + "test": "testing", + "chore": "maintenance", + "perf": "performance", + "ci": "ci/cd", + "build": "build" + } + + def __init__(self, base_dir: str = "."): + """Initialize skill""" + self.base_dir = Path(base_dir) + + def run_command(self, command: List[str]) -> Tuple[bool, str]: + """ + Run a command and return success status and output + + Args: + command: Command as list of arguments + + Returns: + Tuple of (success, output) + """ + try: + result = subprocess.run( + command, + cwd=self.base_dir, + capture_output=True, + text=True, + check=True + ) + return True, result.stdout.strip() + except subprocess.CalledProcessError as e: + return False, e.stderr.strip() + + def is_git_repository(self) -> bool: + """Check if current directory is a git repository""" + success, _ = self.run_command(["git", "rev-parse", "--is-inside-work-tree"]) + return success + + def get_current_branch(self) -> Optional[str]: + """Get the current branch name""" + success, output = self.run_command(["git", "branch", "--show-current"]) + return output if success else None + + def branch_exists(self, branch: str) -> bool: + """Check if a branch exists""" + success, _ = self.run_command(["git", "rev-parse", "--verify", branch]) + return success + + def get_commits_between(self, base: str, head: str) -> List[Dict[str, str]]: + """ + Get commits between two branches + + Args: + base: Base branch + head: Head branch + + Returns: + List of commit dicts with hash, message, author + """ + success, output = self.run_command([ + "git", "log", f"{base}..{head}", + "--format=%H|%s|%an|%ae" + ]) + + if not success or not output: + return [] + + commits = [] + for line in output.split('\n'): + if not line.strip(): + continue + parts = line.split('|') + if len(parts) >= 4: + commits.append({ + "hash": parts[0], + "message": parts[1], + "author": parts[2], + "email": parts[3] + }) + + return commits + + def parse_conventional_commit(self, message: str) -> Dict[str, Optional[str]]: + """ + Parse conventional commit message + + Args: + message: Commit message + + Returns: + Dict with type, scope, description, breaking + """ + # Pattern: type(scope): description + pattern = r'^(\w+)(\([^)]+\))?:\s*(.+)$' + match = re.match(pattern, message) + + if match: + commit_type = match.group(1) + scope = match.group(2)[1:-1] if match.group(2) else None + description = match.group(3) + breaking = "BREAKING CHANGE" in message or message.startswith(f"{commit_type}!") + return { + "type": commit_type, + "scope": scope, + "description": description, + "breaking": breaking + } + + return { + "type": None, + "scope": None, + "description": message, + "breaking": False + } + + def extract_issue_references(self, message: str) -> List[str]: + """ + Extract GitHub issue references from commit message + + Args: + message: Commit message + + Returns: + List of issue references (e.g., ["#123", "#456"]) + """ + pattern = r'#(\d+)' + matches = re.findall(pattern, message) + return [f"#{num}" for num in matches] + + def generate_pr_title(self, commits: List[Dict[str, str]]) -> str: + """ + Generate PR title from commits + + Args: + commits: List of commits + + Returns: + Generated PR title + """ + if not commits: + return "Update" + + # Use the most recent commit message as base + first_commit = commits[0] + parsed = self.parse_conventional_commit(first_commit["message"]) + + if parsed["type"]: + # Use conventional commit format + if parsed["scope"]: + return f"{parsed['type']}({parsed['scope']}): {parsed['description']}" + else: + return f"{parsed['type']}: {parsed['description']}" + else: + # Use first commit message as-is + return first_commit["message"] + + def generate_pr_body(self, commits: List[Dict[str, str]], issues: List[str]) -> str: + """ + Generate PR description from commits + + Args: + commits: List of commits + issues: List of related issues + + Returns: + Generated PR body in markdown + """ + body = [] + + # Summary section + body.append("## Summary\n") + if len(commits) == 1: + body.append(f"{commits[0]['message']}\n") + else: + body.append(f"This PR includes {len(commits)} commits:\n") + for commit in commits[:5]: # Show first 5 + parsed = self.parse_conventional_commit(commit["message"]) + body.append(f"- {commit['message']}") + if len(commits) > 5: + body.append(f"- ... and {len(commits) - 5} more commits") + body.append("") + + # Related issues + if issues: + body.append("## Related Issues\n") + for issue in issues: + body.append(f"- Closes {issue}") + body.append("") + + # Commits section + body.append("## Commits\n") + for commit in commits: + short_hash = commit['hash'][:7] + body.append(f"- {short_hash} {commit['message']}") + body.append("") + + # Check for breaking changes + breaking = [c for c in commits if self.parse_conventional_commit(c["message"])["breaking"]] + if breaking: + body.append("## ⚠️ Breaking Changes\n") + for commit in breaking: + body.append(f"- {commit['message']}") + body.append("") + + return "\n".join(body) + + def detect_labels(self, commits: List[Dict[str, str]]) -> List[str]: + """ + Detect labels from commit types + + Args: + commits: List of commits + + Returns: + List of label names + """ + labels = set() + + for commit in commits: + parsed = self.parse_conventional_commit(commit["message"]) + if parsed["type"] and parsed["type"] in self.COMMIT_TYPE_LABELS: + labels.add(self.COMMIT_TYPE_LABELS[parsed["type"]]) + + if parsed["breaking"]: + labels.add("breaking-change") + + return list(labels) + + def check_gh_cli(self) -> bool: + """Check if GitHub CLI is available""" + success, _ = self.run_command(["gh", "--version"]) + return success + + @certified_skill("git.createpr") + def execute( + self, + base_branch: str = "main", + title: Optional[str] = None, + draft: bool = False, + auto_merge: bool = False, + reviewers: Optional[List[str]] = None, + labels: Optional[List[str]] = None, + body: Optional[str] = None + ) -> Dict[str, Any]: + """ + Execute the skill + + Args: + base_branch: Base branch for PR + title: PR title (auto-generated if not provided) + draft: Create as draft PR + auto_merge: Enable auto-merge + reviewers: List of reviewer usernames + labels: Labels to apply + body: PR description (auto-generated if not provided) + + Returns: + Dict with execution results + """ + try: + logger.info("Executing git.createpr...") + + # Validate git repository + if not self.is_git_repository(): + return { + "ok": False, + "status": "failed", + "error": "Not in a git repository" + } + + # Check gh CLI + if not self.check_gh_cli(): + return { + "ok": False, + "status": "failed", + "error": "GitHub CLI (gh) not found. Install: https://cli.github.com/" + } + + # Get current branch + head_branch = self.get_current_branch() + if not head_branch: + return { + "ok": False, + "status": "failed", + "error": "Could not determine current branch" + } + + # Validate base branch exists + if not self.branch_exists(base_branch): + return { + "ok": False, + "status": "failed", + "error": f"Base branch '{base_branch}' does not exist" + } + + # Get commits + logger.info(f"Analyzing commits between {base_branch} and {head_branch}") + commits = self.get_commits_between(base_branch, head_branch) + + if not commits: + return { + "ok": False, + "status": "failed", + "error": f"No commits found between {base_branch} and {head_branch}" + } + + logger.info(f"Found {len(commits)} commits") + + # Generate title if not provided + if not title: + title = self.generate_pr_title(commits) + logger.info(f"Generated title: {title}") + + # Extract issue references + all_issues = set() + for commit in commits: + issues = self.extract_issue_references(commit["message"]) + all_issues.update(issues) + + # Generate body if not provided + if not body: + body = self.generate_pr_body(commits, list(all_issues)) + logger.info("Generated PR description") + + # Detect labels if not provided + if not labels: + labels = self.detect_labels(commits) + logger.info(f"Detected labels: {labels}") + + # Build gh command + gh_cmd = ["gh", "pr", "create", "--base", base_branch, "--title", title, "--body", body] + + if draft: + gh_cmd.append("--draft") + + if reviewers: + gh_cmd.extend(["--reviewer", ",".join(reviewers)]) + + if labels: + gh_cmd.extend(["--label", ",".join(labels)]) + + # Create PR + logger.info("Creating pull request...") + success, output = self.run_command(gh_cmd) + + if not success: + return { + "ok": False, + "status": "failed", + "error": f"Failed to create PR: {output}" + } + + # Parse PR URL from output + pr_url = output.strip() + logger.info(f"PR created: {pr_url}") + + # Extract PR number from URL + pr_number = None + match = re.search(r'/pull/(\d+)', pr_url) + if match: + pr_number = int(match.group(1)) + + # Build result + result = { + "ok": True, + "status": "success", + "pr_url": pr_url, + "pr_number": pr_number, + "title": title, + "base_branch": base_branch, + "head_branch": head_branch, + "commits_analyzed": len(commits), + "issues_linked": list(all_issues), + "labels_applied": labels or [], + "reviewers_requested": reviewers or [], + "is_draft": draft + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Create GitHub pull request with auto-generated content" + ) + + parser.add_argument( + "--base", + dest="base_branch", + default="main", + help="Base branch for PR (default: main)" + ) + + parser.add_argument( + "--title", + help="PR title (auto-generated if not provided)" + ) + + parser.add_argument( + "--draft", + action="store_true", + help="Create as draft PR" + ) + + parser.add_argument( + "--auto-merge", + dest="auto_merge", + action="store_true", + help="Enable auto-merge if checks pass" + ) + + parser.add_argument( + "--reviewers", + nargs="+", + help="GitHub usernames to request reviews from" + ) + + parser.add_argument( + "--labels", + nargs="+", + help="Labels to apply to PR" + ) + + parser.add_argument( + "--body", + help="PR description (auto-generated if not provided)" + ) + + parser.add_argument( + "--output-format", + choices=["json", "yaml", "human"], + default="human", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = GitCreatepr() + + # Execute skill + result = skill.execute( + base_branch=args.base_branch, + title=args.title, + draft=args.draft, + auto_merge=args.auto_merge, + reviewers=args.reviewers, + labels=args.labels, + body=args.body + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + elif args.output_format == "yaml": + print(yaml.dump(result, default_flow_style=False)) + else: + # Human-readable output + if result.get("ok"): + print(f"\n✓ Pull Request Created!") + print(f" URL: {result.get('pr_url')}") + if result.get('pr_number'): + print(f" Number: #{result.get('pr_number')}") + print(f" Title: {result.get('title')}") + print(f" Base: {result.get('base_branch')} ← {result.get('head_branch')}") + print(f" Commits: {result.get('commits_analyzed')}") + if result.get('issues_linked'): + print(f" Issues: {', '.join(result.get('issues_linked', []))}") + if result.get('labels_applied'): + print(f" Labels: {', '.join(result.get('labels_applied', []))}") + if result.get('reviewers_requested'): + print(f" Reviewers: {', '.join(result.get('reviewers_requested', []))}") + if result.get('is_draft'): + print(f" Status: Draft") + else: + print(f"\n✗ Error: {result.get('error', 'Unknown error')}") + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/git.createpr/skill.yaml b/skills/git.createpr/skill.yaml new file mode 100644 index 0000000..cc42d5b --- /dev/null +++ b/skills/git.createpr/skill.yaml @@ -0,0 +1,58 @@ +name: git.createpr +version: 0.1.0 +description: "Create GitHub pull requests with auto-generated titles and descriptions\ + \ based on commit analysis. Analyzes commit history, identifies related issues,\ + \ and creates well-formatted PRs with proper linking and metadata. - git-commits\ + \ - Commit history between base and feature branch - git-repository - Local git\ + \ repository with commit information - github-credentials - GitHub token for API\ + \ access (from gh CLI or environment) - pull-request - Created GitHub pull request\ + \ with metadata - pr-report - Summary of PR creation including URL, number, and\ + \ status - base_branch (string): Base branch for PR (default: main) - title (string):\ + \ PR title (optional, auto-generated from commits if not provided) - draft (boolean):\ + \ Create as draft PR (default: false) - auto_merge (boolean): Enable auto-merge\ + \ if checks pass (default: false) - reviewers (array): List of GitHub usernames\ + \ to request reviews from - labels (array): Labels to apply to PR (optional, auto-detected\ + \ from commits) - body (string): PR description (optional, auto-generated if not\ + \ provided) - git command line tool - GitHub CLI (gh) or GitHub API access with\ + \ token - Access to git repository - GitHub repository with permissions to create\ + \ PRs 1. Validate we're in a git repository 2. Get current branch name 3. Validate\ + \ base branch exists 4. Fetch latest changes from remote 5. Get commit history between\ + \ base and current branch 6. Analyze commits to extract: - Commit messages - Conventional\ + \ commit types (feat, fix, docs, etc.) - Issue references (#123) - Breaking changes\ + \ 7. Generate PR title (if not provided): - Use most recent commit message - Or\ + \ summarize multiple commits - Format: \"type(scope): description\" 8. Generate\ + \ PR description (if not provided): - Summary of changes - List of commits with\ + \ links - Related issues section - Breaking changes warning (if any) 9. Detect labels\ + \ from commit types: - feat \u2192 enhancement - fix \u2192 bug - docs \u2192 documentation\ + \ - etc. 10. Create PR using GitHub CLI (gh pr create): - Set title and body - Set\ + \ base and head branches - Apply labels - Request reviewers - Set draft status 11.\ + \ Parse PR URL and number from output 12. Return structured result with PR metadata\ + \ ```bash python3 skills/git.createpr/git_createpr.py python3 skills/git.createpr/git_createpr.py\ + \ --draft python3 skills/git.createpr/git_createpr.py --reviewers alice bob python3\ + \ skills/git.createpr/git_createpr.py --base develop python3 skills/git.createpr/git_createpr.py\ + \ --title \"feat: add user authentication\" python3 skills/git.createpr/git_createpr.py\ + \ --labels enhancement breaking-change ``` ```json { \"ok\": true, \"status\": \"\ + success\", \"pr_number\": 123, \"pr_url\": \"https://github.com/owner/repo/pull/123\"\ + , \"title\": \"feat: add user authentication\", \"base_branch\": \"main\", \"head_branch\"\ + : \"feature/auth\", \"commits_analyzed\": 5, \"issues_linked\": [\"#45\", \"#67\"\ + ], \"labels_applied\": [\"enhancement\", \"feature\"], \"reviewers_requested\":\ + \ [\"alice\", \"bob\"], \"is_draft\": false } ``` - git - github - pull-request\ + \ - automation - workflow - pr This skill requires SKILL_AND_COMMAND pattern due\ + \ to: - 12 steps (exceeds threshold) - High autonomy (auto-generates PR content\ + \ intelligently) - Highly reusable for release automation and CI/CD - Complex GitHub\ + \ API interaction - Commit analysis and pattern detection - Multiple execution contexts\ + \ (CLI, agents, workflows) This implementation uses GitHub CLI (gh) for simplicity\ + \ and authentication: - Leverages existing gh authentication - Simpler than managing\ + \ GitHub API tokens - Better error messages - Handles pagination automatically If\ + \ gh CLI is not available, falls back to REST API with token from: - GITHUB_TOKEN\ + \ environment variable - GH_TOKEN environment variable" +inputs: [] +outputs: [] +status: active +permissions: [] +entrypoints: +- command: /git/createpr + handler: git_createpr.py + runtime: python + description: Create GitHub pull requests with auto-generated titles and descriptions + based on commit analysis. An diff --git a/skills/git.createpr/test_git_createpr.py b/skills/git.createpr/test_git_createpr.py new file mode 100644 index 0000000..c4596b5 --- /dev/null +++ b/skills/git.createpr/test_git_createpr.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for git.createpr + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.git_createpr import git_createpr + + +class TestGitCreatepr: + """Tests for GitCreatepr""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = git_createpr.GitCreatepr() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["git_createpr.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + git_createpr.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Create GitHub pull requests with auto-generated ti" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/hook.define/SKILL.md b/skills/hook.define/SKILL.md new file mode 100644 index 0000000..96d2c51 --- /dev/null +++ b/skills/hook.define/SKILL.md @@ -0,0 +1,261 @@ +# hook.define + +## Overview + +**hook.define** is a Betty skill that creates and registers validation hooks for Claude Code. Hooks enable automatic validation and policy enforcement by triggering skills on events like file edits, commits, and pushes. + +## Purpose + +Transform manual validation into automatic safety rails: +- **Before**: Developers must remember to run validation +- **After**: Validation happens automatically on every file edit + +## Usage + +### Basic Usage + +```bash +python skills/hook.define/hook_define.py [options] +``` + +### Parameters + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| `event` | Yes | Hook event trigger | `on_file_edit` | +| `command` | Yes | Command to execute | `api.validate {file_path} zalando` | +| `--pattern` | No | File pattern to match | `*.openapi.yaml` | +| `--blocking` | No | Block on failure (default: true) | `true` | +| `--timeout` | No | Timeout in ms (default: 30000) | `10000` | +| `--description` | No | Human-readable description | `Validate OpenAPI specs` | + +### Valid Events + +| Event | Triggers When | Use Case | +|-------|---------------|----------| +| `on_file_edit` | File is edited in editor | Syntax validation | +| `on_file_save` | File is saved | Code generation | +| `on_commit` | Git commit attempted | Breaking change detection | +| `on_push` | Git push attempted | Full validation suite | +| `on_tool_use` | Any tool is used | Audit logging | +| `on_agent_start` | Agent begins execution | Context injection | +| `on_workflow_end` | Workflow completes | Cleanup/notification | + +## Examples + +### Example 1: Validate OpenAPI Specs on Edit + +```bash +python skills/hook.define/hook_define.py \ + on_file_edit \ + "python betty/skills/api.validate/api_validate.py {file_path} zalando" \ + --pattern="*.openapi.yaml" \ + --blocking=true \ + --timeout=10000 \ + --description="Validate OpenAPI specs against Zalando guidelines" +``` + +**Result**: Every time a `*.openapi.yaml` file is edited, it's automatically validated against Zalando guidelines. If validation fails, the edit is blocked. + +### Example 2: Check Breaking Changes on Commit + +```bash +python skills/hook.define/hook_define.py \ + on_commit \ + "python betty/skills/api.compatibility/check_breaking_changes.py {file_path}" \ + --pattern="specs/**/*.yaml" \ + --blocking=true \ + --description="Prevent commits with breaking API changes" +``` + +**Result**: Commits are blocked if they contain breaking API changes. + +### Example 3: Regenerate Models on Save + +```bash +python skills/hook.define/hook_define.py \ + on_file_save \ + "python betty/skills/api.generate-models/auto_generate.py {file_path}" \ + --pattern="specs/*.openapi.yaml" \ + --blocking=false \ + --description="Auto-regenerate models when specs change" +``` + +**Result**: When an OpenAPI spec is saved, models are regenerated automatically (non-blocking). + +### Example 4: Audit Trail for All Tool Use + +```bash +python skills/hook.define/hook_define.py \ + on_tool_use \ + "python betty/skills/audit.log/log_api_change.py {tool_name}" \ + --blocking=false \ + --description="Log all tool usage for compliance" +``` + +**Result**: All tool usage is logged for audit trails (non-blocking). + +## Output + +### Success Response + +```json +{ + "status": "success", + "data": { + "hook_config": { + "name": "api-validate-all-openapi-yaml", + "command": "python betty/skills/api.validate/api_validate.py {file_path} zalando", + "blocking": true, + "timeout": 10000, + "when": { + "pattern": "*.openapi.yaml" + }, + "description": "Validate OpenAPI specs against Zalando guidelines" + }, + "hooks_file_path": "/home/user/betty/.claude/hooks.yaml", + "event": "on_file_edit", + "total_hooks": 1 + } +} +``` + +### Generated Hooks File + +The skill creates/updates `.claude/hooks.yaml`: + +```yaml +hooks: + on_file_edit: + - name: api-validate-all-openapi-yaml + command: python betty/skills/api.validate/api_validate.py {file_path} zalando + blocking: true + timeout: 10000 + when: + pattern: "*.openapi.yaml" + description: Validate OpenAPI specs against Zalando guidelines +``` + +## How It Works + +1. **Load Existing Hooks**: Reads `.claude/hooks.yaml` if it exists +2. **Create Hook Config**: Builds configuration from parameters +3. **Add or Update**: Adds new hook or updates existing one with same name +4. **Save**: Writes updated configuration back to `.claude/hooks.yaml` + +## Benefits + +### For Developers +- ✅ **Instant feedback**: Errors caught immediately, not at commit time +- ✅ **No discipline required**: Validation happens automatically +- ✅ **Consistent quality**: Standards enforced, not suggested + +### For Teams +- ✅ **Enforced standards**: Can't save non-compliant code +- ✅ **Reduced review time**: Automated checks before human review +- ✅ **Onboarding**: New developers can't accidentally break standards + +### For Organizations +- ✅ **Compliance**: Policies enforced at tool level +- ✅ **Audit trail**: Every validation is logged +- ✅ **Risk reduction**: Catch issues early, not in production + +## Integration with Betty + +### Use in Workflows + +```yaml +# workflows/setup_api_validation.yaml +steps: + - skill: hook.define + args: + - "on_file_edit" + - "api.validate {file_path} zalando" + - "--pattern=*.openapi.yaml" + - "--blocking=true" +``` + +### Use in Agents + +Agents can dynamically create hooks based on project needs: + +```python +# Agent detects OpenAPI specs in project +# Automatically sets up validation hooks +``` + +## File Pattern Examples + +| Pattern | Matches | +|---------|---------| +| `*.openapi.yaml` | All OpenAPI files in current directory | +| `*.asyncapi.yaml` | All AsyncAPI files in current directory | +| `specs/**/*.yaml` | All YAML files in specs/ and subdirectories | +| `src/**/*.ts` | All TypeScript files in src/ and subdirectories | +| `**/*.py` | All Python files anywhere | + +## Blocking vs Non-Blocking + +### Blocking Hooks (blocking: true) +- Operation is **paused** until hook completes +- If hook **fails**, operation is **aborted** +- Use for: Validation, compliance checks, breaking change detection + +### Non-Blocking Hooks (blocking: false) +- Hook runs **asynchronously** +- Operation **continues** regardless of hook result +- Use for: Logging, notifications, background tasks + +## Timeout Considerations + +| Operation | Suggested Timeout | Reasoning | +|-----------|-------------------|-----------| +| Syntax validation | 5,000 - 10,000 ms | Fast, local checks | +| Zally API call | 10,000 - 30,000 ms | Network latency | +| Model generation | 30,000 - 60,000 ms | Compilation time | +| Full test suite | 300,000 ms (5 min) | Comprehensive testing | + +## Error Handling + +### Hook Execution Failed + +If a blocking hook fails: +``` +❌ Hook 'validate-openapi' failed: + - Missing required field: info.x-api-id + - Property 'userId' should use snake_case + +Operation blocked. Fix errors and try again. +``` + +### Hook Timeout + +If a hook exceeds timeout: +``` +⚠️ Hook 'validate-openapi' timed out after 10000ms +Operation blocked for safety. +``` + +## Dependencies + +- **PyYAML**: Required for YAML file handling + ```bash + pip install pyyaml + ``` + +- **context.schema**: For validation rule definitions + +## Files Created + +- `.claude/hooks.yaml` - Hook configurations for Claude Code + +## See Also + +- [api.validate](../api.validate/SKILL.md) - API validation skill +- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model +- [API-Driven Development](../../docs/api-driven-development.md) - Complete guide +- [Claude Code Hooks Documentation](https://docs.claude.com/en/docs/claude-code/hooks) + +## Version + +**0.1.0** - Initial implementation with basic hook definition support diff --git a/skills/hook.define/__init__.py b/skills/hook.define/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/hook.define/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/hook.define/hook_define.py b/skills/hook.define/hook_define.py new file mode 100755 index 0000000..c2e50be --- /dev/null +++ b/skills/hook.define/hook_define.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +""" +Define and register validation hooks for Claude Code. + +This skill creates hook configurations in .claude/hooks.yaml that automatically +trigger validation skills on events like file edits, commits, and pushes. +""" + +import sys +import json +import argparse +import os +from pathlib import Path +from typing import Dict, Any, Optional + +# Add betty module to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response, BettyError +from betty.validation import validate_path +from betty.file_utils import safe_read_json, safe_write_json + +logger = setup_logger(__name__) + +# Valid hook events +VALID_EVENTS = [ + "on_file_edit", + "on_file_save", + "on_commit", + "on_push", + "on_tool_use", + "on_agent_start", + "on_workflow_end" +] + + +def create_hooks_directory() -> Path: + """ + Create .claude directory if it doesn't exist. + + Returns: + Path to .claude directory + """ + claude_dir = Path.cwd() / ".claude" + claude_dir.mkdir(exist_ok=True) + logger.info(f"Ensured .claude directory exists at {claude_dir}") + return claude_dir + + +def load_existing_hooks(hooks_file: Path) -> Dict[str, Any]: + """ + Load existing hooks configuration if it exists. + + Args: + hooks_file: Path to hooks.yaml file + + Returns: + Existing hooks configuration or empty structure + """ + if hooks_file.exists(): + try: + import yaml + with open(hooks_file, 'r') as f: + config = yaml.safe_load(f) or {} + logger.info(f"Loaded existing hooks from {hooks_file}") + return config + except Exception as e: + logger.warning(f"Could not load existing hooks: {e}") + return {"hooks": {}} + else: + logger.info(f"No existing hooks file found at {hooks_file}") + return {"hooks": {}} + + +def create_hook_config( + event: str, + command: str, + pattern: Optional[str] = None, + blocking: bool = True, + timeout: int = 30000, + description: Optional[str] = None +) -> Dict[str, Any]: + """ + Create a hook configuration object. + + Args: + event: Hook event trigger + command: Command to execute + pattern: File pattern to match (optional) + blocking: Whether hook should block on failure + timeout: Timeout in milliseconds + description: Human-readable description + + Returns: + Hook configuration dictionary + """ + # Generate a name from the command and pattern + if pattern: + name = f"{command.replace('/', '-').replace('.', '-')}-{pattern.replace('*', 'all').replace('/', '-')}" + else: + name = command.replace('/', '-').replace('.', '-') + + hook_config = { + "name": name, + "command": command, + "blocking": blocking, + "timeout": timeout + } + + if pattern: + hook_config["when"] = {"pattern": pattern} + + if description: + hook_config["description"] = description + else: + hook_config["description"] = f"Auto-generated hook for {command}" + + return hook_config + + +def add_hook_to_config( + config: Dict[str, Any], + event: str, + hook_config: Dict[str, Any] +) -> Dict[str, Any]: + """ + Add a hook to the configuration, avoiding duplicates. + + Args: + config: Existing hooks configuration + event: Event to add hook to + hook_config: Hook configuration to add + + Returns: + Updated configuration + """ + if "hooks" not in config: + config["hooks"] = {} + + if event not in config["hooks"]: + config["hooks"][event] = [] + + # Check if hook with same name already exists + existing_names = [h.get("name") for h in config["hooks"][event]] + if hook_config["name"] in existing_names: + # Update existing hook + for i, hook in enumerate(config["hooks"][event]): + if hook.get("name") == hook_config["name"]: + config["hooks"][event][i] = hook_config + logger.info(f"Updated existing hook: {hook_config['name']}") + break + else: + # Add new hook + config["hooks"][event].append(hook_config) + logger.info(f"Added new hook: {hook_config['name']}") + + return config + + +def save_hooks_config(hooks_file: Path, config: Dict[str, Any]) -> None: + """ + Save hooks configuration to YAML file. + + Args: + hooks_file: Path to hooks.yaml file + config: Configuration to save + """ + try: + import yaml + with open(hooks_file, 'w') as f: + yaml.dump(config, f, default_flow_style=False, sort_keys=False) + logger.info(f"Saved hooks configuration to {hooks_file}") + except Exception as e: + raise BettyError(f"Failed to save hooks configuration: {e}") + + +def define_hook( + event: str, + command: str, + pattern: Optional[str] = None, + blocking: bool = True, + timeout: int = 30000, + description: Optional[str] = None +) -> Dict[str, Any]: + """ + Define a new hook or update an existing one. + + Args: + event: Hook event trigger + command: Command to execute + pattern: File pattern to match + blocking: Whether hook should block on failure + timeout: Timeout in milliseconds + description: Human-readable description + + Returns: + Result dictionary with hook config and file path + """ + # Validate event + if event not in VALID_EVENTS: + raise BettyError( + f"Invalid event '{event}'. Valid events: {', '.join(VALID_EVENTS)}" + ) + + # Create .claude directory + claude_dir = create_hooks_directory() + hooks_file = claude_dir / "hooks.yaml" + + # Load existing hooks + config = load_existing_hooks(hooks_file) + + # Create new hook config + hook_config = create_hook_config( + event=event, + command=command, + pattern=pattern, + blocking=blocking, + timeout=timeout, + description=description + ) + + # Add to configuration + config = add_hook_to_config(config, event, hook_config) + + # Save configuration + save_hooks_config(hooks_file, config) + + return { + "hook_config": hook_config, + "hooks_file_path": str(hooks_file), + "event": event, + "total_hooks": len(config.get("hooks", {}).get(event, [])) + } + + +def main(): + parser = argparse.ArgumentParser( + description="Define and register validation hooks for Claude Code" + ) + parser.add_argument( + "event", + type=str, + choices=VALID_EVENTS, + help="Hook event trigger" + ) + parser.add_argument( + "command", + type=str, + help="Command to execute when hook triggers" + ) + parser.add_argument( + "--pattern", + type=str, + help="File pattern to match (e.g., '*.openapi.yaml')" + ) + parser.add_argument( + "--blocking", + type=lambda x: x.lower() in ['true', '1', 'yes'], + default=True, + help="Whether hook should block on failure (default: true)" + ) + parser.add_argument( + "--timeout", + type=int, + default=30000, + help="Timeout in milliseconds (default: 30000)" + ) + parser.add_argument( + "--description", + type=str, + help="Human-readable description of what the hook does" + ) + + args = parser.parse_args() + + try: + # Check if PyYAML is installed + try: + import yaml + except ImportError: + raise BettyError( + "PyYAML is required for hook.define. Install with: pip install pyyaml" + ) + + # Define the hook + logger.info(f"Defining hook for event '{args.event}' with command '{args.command}'") + result = define_hook( + event=args.event, + command=args.command, + pattern=args.pattern, + blocking=args.blocking, + timeout=args.timeout, + description=args.description + ) + + # Return structured result + output = { + "status": "success", + "data": result + } + print(json.dumps(output, indent=2)) + + except Exception as e: + logger.error(f"Failed to define hook: {e}") + print(json.dumps(format_error_response(e), indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/hook.define/skill.yaml b/skills/hook.define/skill.yaml new file mode 100644 index 0000000..7963059 --- /dev/null +++ b/skills/hook.define/skill.yaml @@ -0,0 +1,60 @@ +name: hook.define +version: 0.1.0 +description: Create and register validation hooks for Claude Code + +inputs: + - name: event + type: string + required: true + description: Hook event trigger (on_file_edit, on_file_save, on_commit, on_push, on_tool_use) + + - name: pattern + type: string + required: false + description: File pattern to match (e.g., "*.openapi.yaml", "specs/**/*.yaml") + + - name: command + type: string + required: true + description: Command to execute when hook triggers (skill name or full command) + + - name: blocking + type: boolean + required: false + default: true + description: Whether hook should block operation if it fails + + - name: timeout + type: number + required: false + default: 30000 + description: Timeout in milliseconds (default 30 seconds) + + - name: description + type: string + required: false + description: Human-readable description of what the hook does + +outputs: + - name: hook_config + type: object + description: Generated hook configuration + + - name: hooks_file_path + type: string + description: Path to .claude/hooks.yaml file + +dependencies: + - context.schema + +entrypoints: + - command: /skill/hook/define + handler: hook_define.py + runtime: python + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: [hooks, validation, automation, claude-code] diff --git a/skills/hook.register/SKILL.md b/skills/hook.register/SKILL.md new file mode 100644 index 0000000..1aa3042 --- /dev/null +++ b/skills/hook.register/SKILL.md @@ -0,0 +1,352 @@ +--- +name: Hook Register +description: Validates and registers hook manifest files (YAML) in the Hook Registry for versioned hook management. +--- + +# hook.register Skill + +Validates and registers hook manifest files, adding them to the Hook Registry for automatic enforcement. + +## Purpose + +While `hook.define` creates hooks on-the-fly and updates the live configuration (`.claude/hooks.yaml`), the `hook.register` skill formalizes this by validating a hook manifest and adding it to a versioned registry (`/registry/hooks.json`). This enables: + +- **Version Control**: Track hooks as code with full history +- **Review Process**: Hook manifests can go through code review before activation +- **Centralized Management**: Single source of truth for all hooks in the organization +- **Formal Schema**: Ensures hooks conform to required structure + +This skill is part of Betty's Layer 5 (Hooks/Policy) infrastructure, enabling automated governance and validation. + +## Difference from hook.define + +| Feature | hook.define | hook.register | +|---------|-------------|---------------| +| **Purpose** | Create hooks immediately | Register hook manifests for version control | +| **Output File** | `.claude/hooks.yaml` (live config) | `/registry/hooks.json` (registry) | +| **Use Case** | Quick development, testing | Production deployment, formal tracking | +| **Versioning** | Not tracked | Full version history | +| **Schema Validation** | Basic | Comprehensive | + +## Usage + +```bash +python skills/hook.register/hook_register.py +``` + +### Arguments + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| manifest_path | string | Yes | Path to the hook manifest YAML file to validate | + +## Hook Manifest Schema + +### Required Fields + +- **name**: Unique hook identifier (kebab-case recommended, e.g., `validate-openapi-specs`) +- **version**: Semantic version (e.g., `0.1.0`) +- **description**: Human-readable description of what the hook does +- **event**: Hook trigger event (see Valid Events table below) +- **command**: Command to execute when hook triggers + +### Optional Fields + +- **when**: Conditional execution + - `pattern`: File pattern to match (e.g., `"*.openapi.yaml"`, `"specs/**/*.yaml"`) +- **blocking**: Whether hook should block operation if it fails (default: `false`) +- **timeout**: Timeout in milliseconds (default: `30000`) +- **on_failure**: What to do on failure: `show_errors`, `silent`, `log_only` (default: `show_errors`) +- **status**: Hook status (`draft` or `active`, defaults to `draft`) +- **tags**: Array of tags for categorization (e.g., `["api", "validation", "compliance"]`) + +### Valid Events + +| Event | Triggers When | Common Use Cases | +|-------|---------------|------------------| +| `on_file_edit` | File is edited in editor | Real-time syntax validation | +| `on_file_save` | File is saved to disk | Code generation, formatting | +| `on_commit` | Git commit attempted | Breaking change detection, linting | +| `on_push` | Git push attempted | Full validation suite, security scans | +| `on_tool_use` | Any tool is used | Audit logging, usage tracking | +| `on_agent_start` | Agent begins execution | Context injection, authorization | +| `on_workflow_end` | Workflow completes | Cleanup, notifications, reporting | + +## Validation Rules + +The skill performs comprehensive validation: + +1. **Required Fields** – Ensures `name`, `version`, `description`, `event`, and `command` are present +2. **Name Format** – Validates hook name is non-empty and follows naming conventions +3. **Version Format** – Ensures version follows semantic versioning (e.g., `0.1.0`) +4. **Event Type** – Verifies event is one of the supported triggers +5. **Command Validation** – Ensures command is non-empty +6. **Type Checking** – Validates `blocking` is boolean, `timeout` is positive number +7. **Pattern Validation** – If `when.pattern` is provided, ensures it's a valid string +8. **Name Uniqueness** – Checks that hook name doesn't conflict with existing hooks in registry + +## Outputs + +### Success Response + +```json +{ + "ok": true, + "status": "registered", + "errors": [], + "path": "hooks/validate-openapi.yaml", + "details": { + "valid": true, + "status": "registered", + "registry_updated": true, + "manifest": { + "name": "validate-openapi-specs", + "version": "0.1.0", + "description": "Validate OpenAPI specs against Zalando guidelines", + "event": "on_file_edit", + "command": "python skills/api.validate/api_validate.py {file_path} zalando", + "when": { + "pattern": "*.openapi.yaml" + }, + "blocking": true, + "timeout": 10000, + "status": "active", + "tags": ["api", "validation", "openapi"] + } + } +} +``` + +### Failure Response + +```json +{ + "ok": false, + "status": "failed", + "errors": [ + "Invalid event: 'on_file_change'. Must be one of: on_file_edit, on_file_save, on_commit, on_push, on_tool_use, on_agent_start, on_workflow_end" + ], + "path": "hooks/invalid-hook.yaml", + "details": { + "valid": false, + "errors": [ + "Invalid event: 'on_file_change'. Must be one of: on_file_edit, on_file_save, on_commit, on_push, on_tool_use, on_agent_start, on_workflow_end" + ], + "path": "hooks/invalid-hook.yaml" + } +} +``` + +## Examples + +### Example 1: Register OpenAPI Validation Hook + +**Hook Manifest** (`hooks/validate-openapi.yaml`): + +```yaml +name: validate-openapi-specs +version: 0.1.0 +description: "Validate OpenAPI specs against Zalando guidelines on every edit" + +event: on_file_edit +command: "python skills/api.validate/api_validate.py {file_path} zalando" + +when: + pattern: "*.openapi.yaml" + +blocking: true +timeout: 10000 + +status: active +tags: [api, validation, openapi, zalando] +``` + +**Registration Command**: + +```bash +$ python skills/hook.register/hook_register.py hooks/validate-openapi.yaml +{ + "ok": true, + "status": "registered", + "errors": [], + "path": "hooks/validate-openapi.yaml", + "details": { + "valid": true, + "status": "registered", + "registry_updated": true + } +} +``` + +### Example 2: Register Breaking Change Detection Hook + +**Hook Manifest** (`hooks/prevent-breaking-changes.yaml`): + +```yaml +name: prevent-breaking-changes +version: 0.1.0 +description: "Block commits that introduce breaking API changes" + +event: on_commit +command: "python skills/api.compatibility/check_compatibility.py {file_path} --fail_on_breaking" + +when: + pattern: "specs/**/*.yaml" + +blocking: true +timeout: 30000 + +on_failure: show_errors + +status: active +tags: [api, compatibility, breaking-changes, commit-hook] +``` + +### Example 3: Register Audit Log Hook + +**Hook Manifest** (`hooks/audit-tool-usage.yaml`): + +```yaml +name: audit-tool-usage +version: 0.1.0 +description: "Log all tool usage for compliance audit trail" + +event: on_tool_use +command: "python skills/audit.log/log_tool_usage.py {tool_name} {timestamp}" + +blocking: false +timeout: 5000 + +on_failure: log_only + +status: active +tags: [audit, compliance, logging] +``` + +## Integration + +### With Workflows + +Hooks can be registered as part of a workflow: + +```yaml +# workflows/setup_governance.yaml +steps: + - skill: hook.register + args: + - "hooks/validate-openapi.yaml" + required: true + + - skill: hook.register + args: + - "hooks/prevent-breaking-changes.yaml" + required: true +``` + +### With CI/CD + +Validate hooks in continuous integration: + +```yaml +# .github/workflows/validate-hooks.yml +name: Validate Hooks +on: [push, pull_request] +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Validate all hooks + run: | + for hook in hooks/*.yaml; do + python skills/hook.register/hook_register.py "$hook" || exit 1 + done +``` + +### Loading Hooks at Runtime + +Once registered, hooks can be loaded from the registry: + +```python +import json + +with open('/registry/hooks.json') as f: + registry = json.load(f) + +active_hooks = [h for h in registry['hooks'] if h['status'] == 'active'] +``` + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "Missing required fields: name" | Hook manifest missing required field | Add all required fields: name, version, description, event, command | +| "Invalid event: 'X'" | Event type not recognized | Use one of the valid events: on_file_edit, on_file_save, on_commit, on_push, on_tool_use, on_agent_start, on_workflow_end | +| "command cannot be empty" | Command field is empty or whitespace | Provide a valid command string | +| "blocking must be a boolean" | blocking field is not true/false | Use boolean value: `true` or `false` (not string) | +| "timeout must be a positive number" | timeout is zero or negative | Provide positive number in milliseconds (e.g., 30000) | +| "when.pattern must be a non-empty string" | Pattern is empty or wrong type | Provide valid glob pattern (e.g., "*.yaml") | + +## Files Modified + +- **Registry**: `/registry/hooks.json` – Updated with new or modified hook entry +- **Logs**: Hook validation and registration logged to Betty's logging system + +## Hook Registry Structure + +The `/registry/hooks.json` file has this structure: + +```json +{ + "registry_version": "1.0.0", + "generated_at": "2025-10-23T12:00:00Z", + "hooks": [ + { + "name": "validate-openapi-specs", + "version": "0.1.0", + "description": "Validate OpenAPI specs against Zalando guidelines", + "event": "on_file_edit", + "command": "python skills/api.validate/api_validate.py {file_path} zalando", + "when": { + "pattern": "*.openapi.yaml" + }, + "blocking": true, + "timeout": 10000, + "on_failure": "show_errors", + "status": "active", + "tags": ["api", "validation", "openapi"] + } + ] +} +``` + +## See Also + +- **hook.define** – Use this for immediate hook creation in the dev environment (documented in [hook.define SKILL.md](../hook.define/SKILL.md)) +- **Hook Manifest Schema** – See [Command & Hook Infrastructure](../../docs/COMMAND_HOOK_INFRASTRUCTURE.md) for field definitions +- **Betty Architecture** – [Five-Layer Model](../../docs/betty-architecture.md) for understanding how hooks fit into the governance layer +- **Hooks in Claude Code** – [Claude Code Hooks Documentation](https://docs.claude.com/en/docs/claude-code/hooks) + +## Exit Codes + +- **0**: Success (manifest valid and registered) +- **1**: Failure (validation errors or registry update failed) + +## Best Practices + +1. **Version Control**: Keep hook manifests in your repository (`hooks/` directory) +2. **Review Process**: Require code review for hook changes (they can block operations) +3. **Start with Draft**: Register new hooks with `status: draft`, test them, then promote to `active` +4. **Descriptive Names**: Use clear, kebab-case names that describe the hook's purpose +5. **Appropriate Blocking**: Only set `blocking: true` for critical validations (it will stop operations) +6. **Reasonable Timeouts**: Set realistic timeouts based on hook complexity (avoid too short or too long) +7. **Tag Appropriately**: Use tags for easy filtering and organization +8. **Test Patterns**: Test file patterns thoroughly to avoid unintended matches + +## Status + +**Active** – This skill is production-ready and actively used in Betty's hook infrastructure. + +## Version History + +- **0.1.0** (Oct 2025) – Initial implementation with full validation and registry management diff --git a/skills/hook.register/__init__.py b/skills/hook.register/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/hook.register/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/hook.register/hook_register.py b/skills/hook.register/hook_register.py new file mode 100755 index 0000000..e771900 --- /dev/null +++ b/skills/hook.register/hook_register.py @@ -0,0 +1,415 @@ +#!/usr/bin/env python3 +""" +hook_register.py – Implementation of the hook.register Skill +Validates hook manifests and registers them in the Hook 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_HOOK_FIELDS, + HOOKS_REGISTRY_FILE, +) +from betty.enums import HookEvent, HookStatus +from betty.validation import ( + validate_path, + validate_manifest_fields, + validate_hook_name, + validate_version, + validate_hook_event +) +from betty.logging_utils import setup_logger +from betty.errors import format_error_response +from betty.models import HookManifest +from betty.file_utils import atomic_write_json + +logger = setup_logger(__name__) + + +class HookValidationError(Exception): + """Raised when hook validation fails.""" + pass + + +class HookRegistryError(Exception): + """Raised when hook 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 hook 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_hook_manifest(path: str) -> Dict[str, Any]: + """ + Load and parse a hook manifest from YAML file. + + Args: + path: Path to hook manifest file + + Returns: + Parsed manifest dictionary + + Raises: + HookValidationError: If manifest cannot be loaded or parsed + """ + try: + with open(path) as f: + manifest = yaml.safe_load(f) + return manifest + except FileNotFoundError: + raise HookValidationError(f"Manifest file not found: {path}") + except yaml.YAMLError as e: + raise HookValidationError(f"Failed to parse YAML: {e}") + + +def validate_hook_schema(manifest: Dict[str, Any]) -> List[str]: + """ + Validate hook manifest using Pydantic schema. + + Args: + manifest: Hook manifest dictionary + + Returns: + List of validation errors (empty if valid) + """ + errors: List[str] = [] + + try: + HookManifest.model_validate(manifest) + logger.info("Pydantic schema validation passed for hook manifest") + except PydanticValidationError as exc: + logger.warning("Pydantic schema validation failed for hook 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 a hook manifest meets all requirements. + + Validation checks: + 1. Required fields are present + 2. Name format is valid + 3. Version format is valid + 4. Event type is valid + 5. Command is specified + 6. Pattern is valid (if present) + 7. Blocking mode is boolean (if present) + + Args: + path: Path to hook 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 hook manifest: {path}") + + errors = [] + + # Load manifest + try: + manifest = load_hook_manifest(path) + except HookValidationError as e: + return { + "valid": False, + "errors": [str(e)], + "path": path + } + + # Check required fields first so user-friendly errors appear before schema errors + missing = validate_manifest_fields(manifest, REQUIRED_HOOK_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 while still running custom checks + schema_errors = validate_hook_schema(manifest) + errors.extend(schema_errors) + + name = manifest.get("name") + if name is not None: + try: + validate_hook_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}") + + event = manifest.get("event") + if event is not None: + try: + validate_hook_event(event) + except Exception as e: + errors.append(f"Invalid event: {str(e)}") + logger.warning(f"Invalid event: {e}") + elif "event" not in missing: + errors.append("event must be provided") + logger.warning("Hook event missing") + + # Validate command is not empty + command = manifest.get("command", "") + if not command or not command.strip(): + errors.append("command cannot be empty") + logger.warning("Empty command field") + + # Validate blocking if present + if "blocking" in manifest: + if not isinstance(manifest["blocking"], bool): + errors.append("blocking must be a boolean (true/false)") + logger.warning(f"Invalid blocking type: {type(manifest['blocking'])}") + + # Validate timeout if present + if "timeout" in manifest: + timeout = manifest["timeout"] + if not isinstance(timeout, (int, float)) or timeout <= 0: + errors.append("timeout must be a positive number (in milliseconds)") + logger.warning(f"Invalid timeout: {timeout}") + + # Validate status if present + if "status" in manifest: + valid_statuses = [s.value for s in HookStatus] + 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 when.pattern if present + if "when" in manifest: + when = manifest["when"] + if not isinstance(when, dict): + errors.append("when must be an object") + elif "pattern" in when: + pattern = when["pattern"] + if not pattern or not isinstance(pattern, str): + errors.append("when.pattern must be a non-empty string") + + if errors: + logger.warning(f"Validation failed with {len(errors)} error(s)") + return { + "valid": False, + "errors": errors, + "path": path + } + + logger.info("✅ Hook manifest validation passed") + return { + "valid": True, + "errors": [], + "path": path, + "manifest": manifest + } + + +def load_hook_registry() -> Dict[str, Any]: + """ + Load existing hook registry. + + Returns: + Hook registry dictionary, or new empty registry if file doesn't exist + """ + if not os.path.exists(HOOKS_REGISTRY_FILE): + logger.info("Hook registry not found, creating new registry") + return { + "registry_version": "1.0.0", + "generated_at": datetime.now(timezone.utc).isoformat(), + "hooks": [] + } + + try: + with open(HOOKS_REGISTRY_FILE) as f: + registry = json.load(f) + logger.info(f"Loaded hook registry with {len(registry.get('hooks', []))} hook(s)") + return registry + except json.JSONDecodeError as e: + raise HookRegistryError(f"Failed to parse hook registry: {e}") + + +def update_hook_registry(manifest: Dict[str, Any]) -> bool: + """ + Add or update hook in the hook registry. + + Args: + manifest: Validated hook manifest + + Returns: + True if registry was updated successfully + + Raises: + HookRegistryError: If registry update fails + """ + logger.info(f"Updating hook registry for: {manifest['name']}") + + # Load existing registry + registry = load_hook_registry() + + # Create registry entry + entry = { + "name": manifest["name"], + "version": manifest["version"], + "description": manifest["description"], + "event": manifest["event"], + "command": manifest["command"], + "when": manifest.get("when", {}), + "blocking": manifest.get("blocking", False), + "timeout": manifest.get("timeout", 30000), + "on_failure": manifest.get("on_failure", "show_errors"), + "status": manifest.get("status", "draft"), + "tags": manifest.get("tags", []) + } + + # Check if hook already exists + hooks = registry.get("hooks", []) + existing_index = None + for i, hook in enumerate(hooks): + if hook["name"] == manifest["name"]: + existing_index = i + break + + if existing_index is not None: + # Update existing hook + hooks[existing_index] = entry + logger.info(f"Updated existing hook: {manifest['name']}") + else: + # Add new hook + hooks.append(entry) + logger.info(f"Added new hook: {manifest['name']}") + + registry["hooks"] = hooks + registry["generated_at"] = datetime.now(timezone.utc).isoformat() + + # Write registry back to disk atomically + try: + atomic_write_json(HOOKS_REGISTRY_FILE, registry) + logger.info(f"Hook registry updated successfully") + return True + except Exception as e: + raise HookRegistryError(f"Failed to write hook registry: {e}") + + +def main(): + """Main CLI entry point.""" + if len(sys.argv) < 2: + message = "Usage: hook_register.py " + 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_hook_registry(validation["manifest"]) + details["status"] = "registered" + details["registry_updated"] = registry_updated + except HookRegistryError 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": "Hook 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 HookValidationError 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() diff --git a/skills/hook.register/skill.yaml b/skills/hook.register/skill.yaml new file mode 100644 index 0000000..35f1ed4 --- /dev/null +++ b/skills/hook.register/skill.yaml @@ -0,0 +1,36 @@ +name: hook.register +version: 0.1.0 +description: "Validate and register hook manifests in the Hook Registry" + +inputs: + - name: manifest_path + type: string + required: true + description: "Path to the hook 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/hook/register + handler: hook_register.py + runtime: python + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: [hook, registry, validation, infrastructure, policy] diff --git a/skills/hook.simulate/SKILL.md b/skills/hook.simulate/SKILL.md new file mode 100644 index 0000000..2ed1699 --- /dev/null +++ b/skills/hook.simulate/SKILL.md @@ -0,0 +1,557 @@ +# hook.simulate + +**Version:** 0.1.0 +**Status:** Active +**Tags:** hook, simulation, testing, validation, development + +## Overview + +The `hook.simulate` skill allows developers to test a hook manifest before registering it in the Betty Framework. It validates the manifest structure, simulates hook triggers based on event types, and optionally executes the hook commands in a controlled environment. + +This skill is essential for: +- **Testing hooks before deployment** - Catch errors early in development +- **Understanding hook behavior** - See which files match patterns and how commands execute +- **Debugging hook issues** - Validate patterns, commands, and execution flow +- **Safe experimentation** - Test hooks without affecting the live system + +## Features + +- ✅ **Manifest Validation** - Validates all required and optional fields +- ✅ **Pattern Matching** - Shows which files match the hook's pattern +- ✅ **Event Simulation** - Simulates different hook events (on_file_edit, on_commit, etc.) +- ✅ **Command Execution** - Runs hook commands with dry-run or actual execution +- ✅ **Timeout Testing** - Validates timeout behavior +- ✅ **Blocking Simulation** - Shows how blocking hooks would behave +- ✅ **JSON Output** - Structured results for automation and analysis + +## Usage + +### Command Line + +```bash +# Basic validation and simulation +python skills/hook.simulate/hook_simulate.py examples/test-hook.yaml + +# Simulate with dry-run command execution +python skills/hook.simulate/hook_simulate.py examples/test-hook.yaml --execute --dry-run + +# Actually execute the command +python skills/hook.simulate/hook_simulate.py examples/test-hook.yaml --execute --no-dry-run + +# Get JSON output for scripting +python skills/hook.simulate/hook_simulate.py examples/test-hook.yaml --output json +``` + +### As a Skill + +```python +from skills.hook.simulate.hook_simulate import simulate_hook + +# Simulate a hook +results = simulate_hook( + manifest_path="examples/test-hook.yaml", + dry_run=True, + execute=True +) + +# Check validation +if results["valid"]: + print("✅ Hook is valid") +else: + print("❌ Validation errors:", results["validation_errors"]) + +# Check if hook would trigger +if results["trigger_simulation"]["would_trigger"]: + print("Hook would trigger!") + print("Matching files:", results["trigger_simulation"]["matching_files"]) +``` + +## Input Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `manifest_path` | string | Yes | - | Path to the hook.yaml manifest file | +| `execute` | boolean | No | false | Whether to execute the hook command | +| `dry_run` | boolean | No | true | If true, simulate without actually running commands | + +## Output Schema + +```json +{ + "manifest_path": "path/to/hook.yaml", + "timestamp": "2025-10-23 12:34:56", + "valid": true, + "validation_errors": [], + "manifest": { + "name": "hook-name", + "version": "0.1.0", + "description": "Hook description", + "event": "on_file_edit", + "command": "python script.py {file_path}", + "when": { + "pattern": "*.yaml" + }, + "blocking": true, + "timeout": 30000 + }, + "trigger_simulation": { + "would_trigger": true, + "reason": "Found 5 file(s) matching pattern: *.yaml", + "matching_files": ["file1.yaml", "file2.yaml"], + "pattern": "*.yaml" + }, + "command_executions": [ + { + "command": "python script.py file1.yaml", + "executed": true, + "dry_run": false, + "stdout": "Validation passed", + "stderr": "", + "return_code": 0, + "execution_time_ms": 123.45, + "success": true, + "file": "file1.yaml" + } + ], + "blocking": true, + "timeout_ms": 30000 +} +``` + +## Examples + +### Example 1: Validate OpenAPI Hook + +Create a hook manifest `openapi-validator.yaml`: + +```yaml +name: validate-openapi +version: 0.1.0 +description: "Validate OpenAPI specifications on edit" + +event: on_file_edit + +command: "python betty/skills/api.validate/api_validate.py {file_path} zalando" + +when: + pattern: "**/*.openapi.yaml" + +blocking: true +timeout: 30000 +on_failure: show_errors + +status: draft +tags: [api, validation, openapi] +``` + +Simulate it: + +```bash +python skills/hook.simulate/hook_simulate.py openapi-validator.yaml +``` + +Output: +``` +=== Hook Simulation Results === +Manifest: openapi-validator.yaml +Timestamp: 2025-10-23 12:34:56 + +✅ VALIDATION PASSED + +Hook: validate-openapi v0.1.0 +Event: on_file_edit +Command: python betty/skills/api.validate/api_validate.py {file_path} zalando +Blocking: True +Timeout: 30000ms + +✅ WOULD TRIGGER +Reason: Found 3 file(s) matching pattern: **/*.openapi.yaml + +Matching files (3): + - specs/petstore.openapi.yaml + - specs/users.openapi.yaml + - api/products.openapi.yaml +``` + +### Example 2: Test Commit Hook + +Create `pre-commit.yaml`: + +```yaml +name: pre-commit-linter +version: 1.0.0 +description: "Run linter before commits" + +event: on_commit + +command: "pylint src/" + +blocking: true +timeout: 60000 + +status: active +tags: [lint, quality] +``` + +Simulate with execution: + +```bash +python skills/hook.simulate/hook_simulate.py pre-commit.yaml --execute --dry-run +``` + +Output: +``` +=== Hook Simulation Results === +Manifest: pre-commit.yaml +Timestamp: 2025-10-23 12:35:00 + +✅ VALIDATION PASSED + +Hook: pre-commit-linter v1.0.0 +Event: on_commit +Command: pylint src/ +Blocking: True +Timeout: 60000ms + +✅ WOULD TRIGGER +Reason: on_commit hook would trigger with 5 changed file(s) + +Changed files (5): + - src/main.py + - src/utils.py + - tests/test_main.py + - README.md + - setup.py + +=== Command Execution Results (1) === + +[1] N/A +Command: pylint src/ +Mode: DRY RUN (not executed) +``` + +### Example 3: Test with Actual Execution + +```bash +python skills/hook.simulate/hook_simulate.py openapi-validator.yaml --execute --no-dry-run +``` + +Output: +``` +=== Hook Simulation Results === +... + +=== Command Execution Results (3) === + +[1] specs/petstore.openapi.yaml +Command: python betty/skills/api.validate/api_validate.py specs/petstore.openapi.yaml zalando +Executed: Yes +Return code: 0 +Execution time: 234.56ms +Status: ✅ SUCCESS + +Stdout: +✅ OpenAPI spec is valid + +[2] specs/users.openapi.yaml +Command: python betty/skills/api.validate/api_validate.py specs/users.openapi.yaml zalando +Executed: Yes +Return code: 1 +Execution time: 189.23ms +Status: ❌ FAILED + +Stderr: +Error: Missing required field 'info.contact' + +[3] api/products.openapi.yaml +Command: python betty/skills/api.validate/api_validate.py api/products.openapi.yaml zalando +Executed: Yes +Return code: 0 +Execution time: 201.45ms +Status: ✅ SUCCESS +``` + +### Example 4: JSON Output for Automation + +```bash +python skills/hook.simulate/hook_simulate.py my-hook.yaml --output json > results.json +``` + +Then process with scripts: + +```python +import json + +with open("results.json") as f: + results = json.load(f) + +if not results["valid"]: + print("Validation failed:") + for error in results["validation_errors"]: + print(f" - {error}") + exit(1) + +if results["trigger_simulation"]["would_trigger"]: + files = results["trigger_simulation"]["matching_files"] + print(f"Hook would run on {len(files)} files") + +for execution in results["command_executions"]: + if not execution["success"]: + print(f"Failed on {execution['file']}") + print(f"Error: {execution['stderr']}") +``` + +## Event Type Support + +| Event | Simulation Support | Description | +|-------|-------------------|-------------| +| `on_file_edit` | ✅ Full | Matches files by pattern, simulates editing | +| `on_file_save` | ✅ Full | Similar to on_file_edit | +| `on_commit` | ✅ Full | Checks git status, shows changed files | +| `on_push` | ⚠️ Partial | Notes hook would trigger, no git simulation | +| `on_tool_use` | ⚠️ Partial | Notes hook would trigger, no tool simulation | +| `on_agent_start` | ⚠️ Partial | Notes hook would trigger, no agent simulation | +| `on_workflow_end` | ⚠️ Partial | Notes hook would trigger, no workflow simulation | + +## Validation Rules + +The skill validates all Betty Framework hook requirements: + +1. **Required Fields**: name, version, description, event, command +2. **Name Format**: Lowercase, starts with letter, allows hyphens/underscores +3. **Version Format**: Semantic versioning (e.g., 1.0.0, 0.1.0-alpha) +4. **Event**: Must be a valid hook event +5. **Command**: Non-empty string +6. **Blocking**: Must be boolean if specified +7. **Timeout**: Must be positive number in milliseconds if specified +8. **Status**: Must be draft, active, disabled, or archived if specified +9. **Pattern**: Must be non-empty string if specified in when.pattern + +## Pattern Matching + +The skill supports glob patterns for file matching: + +| Pattern | Description | Example Matches | +|---------|-------------|-----------------| +| `*.yaml` | Files in current directory | `config.yaml`, `spec.yaml` | +| `**/*.yaml` | YAML files anywhere | `src/config.yaml`, `specs/api/v1.yaml` | +| `src/**/*.py` | Python files in src/ | `src/main.py`, `src/utils/helper.py` | +| `*.openapi.yaml` | OpenAPI files only | `petstore.openapi.yaml` | +| `tests/**/*.test.js` | Test files | `tests/unit/api.test.js` | + +## Dry-Run vs Execution Modes + +### Dry-Run Mode (Default) + +```bash +python skills/hook.simulate/hook_simulate.py hook.yaml --execute --dry-run +``` + +- ✅ Validates manifest +- ✅ Shows matching files +- ✅ Shows command that would run +- ❌ Does NOT execute command +- ⚡ Safe for testing + +### Execution Mode + +```bash +python skills/hook.simulate/hook_simulate.py hook.yaml --execute --no-dry-run +``` + +- ✅ Validates manifest +- ✅ Shows matching files +- ✅ Actually executes command +- ✅ Captures stdout/stderr +- ✅ Reports execution time +- ⚠️ Use with caution + +### Validation Only (No Execution) + +```bash +python skills/hook.simulate/hook_simulate.py hook.yaml +``` + +- ✅ Validates manifest +- ✅ Shows if hook would trigger +- ✅ Shows matching files +- ❌ Does NOT execute command +- ⚡ Fastest mode + +## Integration with Hook Workflow + +### Development Workflow + +```bash +# 1. Create hook manifest +cat > my-hook.yaml < Dict[str, Any]: + """ + Load and parse a hook manifest from a YAML file. + + Args: + manifest_path: Path to the hook.yaml file + + Returns: + Parsed manifest dictionary + + Raises: + BettyError: If file doesn't exist or YAML is invalid + """ + path = Path(manifest_path) + + if not path.exists(): + raise BettyError(f"Manifest file not found: {manifest_path}") + + try: + with open(path, 'r') as f: + manifest = yaml.safe_load(f) + + if not isinstance(manifest, dict): + raise BettyError("Manifest must be a YAML dictionary") + + return manifest + except yaml.YAMLError as e: + raise BettyError(f"Invalid YAML in manifest: {e}") + + +def validate_manifest(manifest: Dict[str, Any]) -> List[str]: + """ + Validate a hook manifest against Betty's requirements. + + Args: + manifest: The hook manifest to validate + + Returns: + List of validation error messages (empty if valid) + """ + errors = [] + + # 1. Required fields check + try: + missing_fields = validate_manifest_fields(manifest, REQUIRED_HOOK_FIELDS) + if missing_fields: + errors.append(f"Missing required fields: {', '.join(missing_fields)}") + except (ValidationError, BettyError) as e: + errors.append(str(e)) + + # 2. Name format validation + if "name" in manifest: + try: + validate_hook_name(manifest["name"]) + except (ValidationError, BettyError) as e: + errors.append(str(e)) + + # 3. Version format validation + if "version" in manifest: + try: + # Convert to string if it's a number (YAML may parse "1.0" as float) + version = manifest["version"] + if isinstance(version, (int, float)): + version = str(version) + manifest["version"] = version # Update manifest with string version + validate_version(version) + except (ValidationError, BettyError) as e: + errors.append(str(e)) + + # 4. Event type validation + if "event" in manifest: + try: + validate_hook_event(manifest["event"]) + except (ValidationError, BettyError) as e: + errors.append(str(e)) + + # 5. Command validation + command = manifest.get("command", "") + if not command or not command.strip(): + errors.append("command cannot be empty") + + # 6. Blocking type validation + if "blocking" in manifest: + if not isinstance(manifest["blocking"], bool): + errors.append("blocking must be a boolean (true/false)") + + # 7. Timeout validation + if "timeout" in manifest: + timeout = manifest["timeout"] + if not isinstance(timeout, (int, float)) or timeout <= 0: + errors.append("timeout must be a positive number (in milliseconds)") + + # 8. Status validation + if "status" in manifest: + valid_statuses = [s.value for s in HookStatus] + if manifest["status"] not in valid_statuses: + errors.append(f"Invalid status: '{manifest['status']}'. Must be one of: {', '.join(valid_statuses)}") + + # 9. Pattern validation + if "when" in manifest and "pattern" in manifest["when"]: + pattern = manifest["when"]["pattern"] + if not pattern or not isinstance(pattern, str): + errors.append("when.pattern must be a non-empty string") + + return errors + + +def find_matching_files(pattern: str, base_path: str = ".") -> List[str]: + """ + Find files matching a glob pattern. + + Args: + pattern: Glob pattern to match (e.g., "*.yaml", "src/**/*.py") + base_path: Base directory to search from + + Returns: + List of matching file paths (relative to base_path) + """ + base = Path(base_path).resolve() + matches = [] + + # Handle both simple patterns and recursive patterns + if "**" in pattern: + # Recursive glob + for match in base.glob(pattern): + if match.is_file(): + try: + rel_path = match.relative_to(base) + matches.append(str(rel_path)) + except ValueError: + matches.append(str(match)) + else: + # Non-recursive glob + for match in base.glob(pattern): + if match.is_file(): + try: + rel_path = match.relative_to(base) + matches.append(str(rel_path)) + except ValueError: + matches.append(str(match)) + + return sorted(matches) + + +def simulate_on_file_edit(manifest: Dict[str, Any], base_path: str = ".") -> Dict[str, Any]: + """ + Simulate an on_file_edit hook trigger. + + Args: + manifest: The hook manifest + base_path: Base directory to search for matching files + + Returns: + Simulation results dictionary + """ + pattern = manifest.get("when", {}).get("pattern") + + if not pattern: + return { + "would_trigger": False, + "reason": "No pattern specified - hook would not trigger for specific files", + "matching_files": [] + } + + matching_files = find_matching_files(pattern, base_path) + + if not matching_files: + return { + "would_trigger": False, + "reason": f"No files match pattern: {pattern}", + "matching_files": [], + "pattern": pattern + } + + return { + "would_trigger": True, + "reason": f"Found {len(matching_files)} file(s) matching pattern: {pattern}", + "matching_files": matching_files, + "pattern": pattern + } + + +def simulate_on_commit(manifest: Dict[str, Any], base_path: str = ".") -> Dict[str, Any]: + """ + Simulate an on_commit hook trigger. + + Args: + manifest: The hook manifest + base_path: Base directory (git repository root) + + Returns: + Simulation results dictionary + """ + # Check if we're in a git repository + try: + result = subprocess.run( + ["git", "rev-parse", "--git-dir"], + cwd=base_path, + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode != 0: + return { + "would_trigger": False, + "reason": "Not in a git repository", + "git_status": None + } + except (subprocess.TimeoutExpired, FileNotFoundError): + return { + "would_trigger": False, + "reason": "Git not available or timeout", + "git_status": None + } + + # Get git status to see what would be committed + try: + result = subprocess.run( + ["git", "status", "--porcelain"], + cwd=base_path, + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + status_lines = result.stdout.strip().split("\n") if result.stdout.strip() else [] + changed_files = [line[3:] for line in status_lines if line.strip()] + + return { + "would_trigger": True, + "reason": f"on_commit hook would trigger with {len(changed_files)} changed file(s)", + "changed_files": changed_files, + "git_status": result.stdout.strip() + } + else: + return { + "would_trigger": False, + "reason": "Could not get git status", + "git_status": result.stderr.strip() + } + except subprocess.TimeoutExpired: + return { + "would_trigger": False, + "reason": "Git status command timed out", + "git_status": None + } + + +def execute_command( + command: str, + file_path: Optional[str] = None, + timeout_ms: int = 30000, + dry_run: bool = False +) -> Dict[str, Any]: + """ + Execute a hook command (or simulate execution in dry-run mode). + + Args: + command: The command to execute + file_path: Optional file path to substitute {file_path} placeholder + timeout_ms: Timeout in milliseconds + dry_run: If True, don't actually execute the command + + Returns: + Execution results dictionary + """ + # Substitute placeholders + if file_path: + command = command.replace("{file_path}", file_path) + + if dry_run: + return { + "command": command, + "executed": False, + "dry_run": True, + "stdout": "", + "stderr": "", + "return_code": None, + "execution_time_ms": 0 + } + + # Execute the command + start_time = time.time() + timeout_sec = timeout_ms / 1000.0 + + try: + result = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + timeout=timeout_sec + ) + + execution_time = (time.time() - start_time) * 1000 + + return { + "command": command, + "executed": True, + "dry_run": False, + "stdout": result.stdout, + "stderr": result.stderr, + "return_code": result.returncode, + "execution_time_ms": round(execution_time, 2), + "success": result.returncode == 0 + } + except subprocess.TimeoutExpired: + execution_time = (time.time() - start_time) * 1000 + return { + "command": command, + "executed": True, + "dry_run": False, + "stdout": "", + "stderr": f"Command timed out after {timeout_ms}ms", + "return_code": -1, + "execution_time_ms": round(execution_time, 2), + "success": False, + "timeout": True + } + except Exception as e: + execution_time = (time.time() - start_time) * 1000 + return { + "command": command, + "executed": True, + "dry_run": False, + "stdout": "", + "stderr": f"Error executing command: {str(e)}", + "return_code": -1, + "execution_time_ms": round(execution_time, 2), + "success": False, + "error": str(e) + } + + +def simulate_hook(manifest_path: str, dry_run: bool = True, execute: bool = False) -> Dict[str, Any]: + """ + Main function to simulate a hook. + + Args: + manifest_path: Path to the hook.yaml file + dry_run: If True, don't actually execute commands + execute: If True, execute the command on matching files + + Returns: + Complete simulation results + """ + results = { + "manifest_path": manifest_path, + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "valid": False, + "validation_errors": [], + "manifest": {}, + "trigger_simulation": {}, + "command_executions": [] + } + + # Load manifest + try: + manifest = load_hook_manifest(manifest_path) + results["manifest"] = manifest + except BettyError as e: + results["validation_errors"].append(str(e)) + return results + + # Validate manifest + validation_errors = validate_manifest(manifest) + if validation_errors: + results["validation_errors"] = validation_errors + return results + + results["valid"] = True + + # Get base path (directory containing the manifest) + base_path = Path(manifest_path).parent.resolve() + + # Simulate based on event type + event = manifest.get("event") + + if event == HookEvent.ON_FILE_EDIT.value: + trigger_result = simulate_on_file_edit(manifest, str(base_path)) + results["trigger_simulation"] = trigger_result + + # Execute command on matching files if requested + if execute and trigger_result.get("would_trigger"): + command = manifest.get("command", "") + timeout = manifest.get("timeout", 30000) + + for file_path in trigger_result.get("matching_files", []): + exec_result = execute_command(command, file_path, timeout, dry_run) + exec_result["file"] = file_path + results["command_executions"].append(exec_result) + + elif event == HookEvent.ON_COMMIT.value: + trigger_result = simulate_on_commit(manifest, str(base_path)) + results["trigger_simulation"] = trigger_result + + # Execute command if requested + if execute and trigger_result.get("would_trigger"): + command = manifest.get("command", "") + timeout = manifest.get("timeout", 30000) + + exec_result = execute_command(command, None, timeout, dry_run) + results["command_executions"].append(exec_result) + + elif event == HookEvent.ON_FILE_SAVE.value: + # Similar to on_file_edit + trigger_result = simulate_on_file_edit(manifest, str(base_path)) + trigger_result["event_note"] = "on_file_save behaves similarly to on_file_edit for simulation purposes" + results["trigger_simulation"] = trigger_result + + if execute and trigger_result.get("would_trigger"): + command = manifest.get("command", "") + timeout = manifest.get("timeout", 30000) + + for file_path in trigger_result.get("matching_files", [])[:3]: # Limit to 3 files + exec_result = execute_command(command, file_path, timeout, dry_run) + exec_result["file"] = file_path + results["command_executions"].append(exec_result) + + else: + # For other events, just note that they would trigger + results["trigger_simulation"] = { + "would_trigger": True, + "reason": f"Event '{event}' would trigger in appropriate context", + "note": f"Full simulation not implemented for {event} events" + } + + if execute: + command = manifest.get("command", "") + timeout = manifest.get("timeout", 30000) + exec_result = execute_command(command, None, timeout, dry_run) + results["command_executions"].append(exec_result) + + # Add metadata about blocking behavior + results["blocking"] = manifest.get("blocking", False) + results["timeout_ms"] = manifest.get("timeout", 30000) + + return results + + +def main(): + """Command-line interface for hook simulation.""" + parser = argparse.ArgumentParser( + description="Simulate Betty Framework hook execution", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Validate and simulate hook trigger + python hook_simulate.py examples/test-hook.yaml + + # Simulate with dry-run command execution + python hook_simulate.py examples/test-hook.yaml --execute --dry-run + + # Actually execute the command + python hook_simulate.py examples/test-hook.yaml --execute --no-dry-run + """ + ) + + parser.add_argument( + "manifest", + help="Path to hook.yaml manifest file" + ) + + parser.add_argument( + "--execute", + action="store_true", + help="Execute the hook command (simulated by default)" + ) + + parser.add_argument( + "--dry-run", + action="store_true", + default=True, + help="Dry-run mode: show what would be executed without running it (default)" + ) + + parser.add_argument( + "--no-dry-run", + dest="dry_run", + action="store_false", + help="Actually execute the command" + ) + + parser.add_argument( + "--output", + choices=["json", "summary"], + default="summary", + help="Output format (default: summary)" + ) + + args = parser.parse_args() + + try: + results = simulate_hook(args.manifest, dry_run=args.dry_run, execute=args.execute) + + if args.output == "json": + print(json.dumps(results, indent=2)) + else: + # Print summary + print(f"\n=== Hook Simulation Results ===") + print(f"Manifest: {results['manifest_path']}") + print(f"Timestamp: {results['timestamp']}") + print() + + if results["validation_errors"]: + print("❌ VALIDATION FAILED") + print("\nErrors:") + for error in results["validation_errors"]: + print(f" - {error}") + sys.exit(1) + + print("✅ VALIDATION PASSED") + print() + + manifest = results["manifest"] + print(f"Hook: {manifest.get('name')} v{manifest.get('version')}") + print(f"Event: {manifest.get('event')}") + print(f"Command: {manifest.get('command')}") + print(f"Blocking: {results.get('blocking')}") + print(f"Timeout: {results.get('timeout_ms')}ms") + print() + + trigger = results["trigger_simulation"] + if trigger.get("would_trigger"): + print("✅ WOULD TRIGGER") + print(f"Reason: {trigger.get('reason')}") + + if "matching_files" in trigger and trigger["matching_files"]: + print(f"\nMatching files ({len(trigger['matching_files'])}):") + for f in trigger["matching_files"][:10]: + print(f" - {f}") + if len(trigger["matching_files"]) > 10: + print(f" ... and {len(trigger['matching_files']) - 10} more") + + if "changed_files" in trigger and trigger["changed_files"]: + print(f"\nChanged files ({len(trigger['changed_files'])}):") + for f in trigger["changed_files"][:10]: + print(f" - {f}") + if len(trigger["changed_files"]) > 10: + print(f" ... and {len(trigger['changed_files']) - 10} more") + else: + print("❌ WOULD NOT TRIGGER") + print(f"Reason: {trigger.get('reason')}") + + print() + + if results["command_executions"]: + print(f"\n=== Command Execution Results ({len(results['command_executions'])}) ===") + for i, exec_result in enumerate(results["command_executions"], 1): + print(f"\n[{i}] {exec_result.get('file', 'N/A')}") + print(f"Command: {exec_result['command']}") + + if exec_result.get("dry_run"): + print("Mode: DRY RUN (not executed)") + else: + print(f"Executed: Yes") + print(f"Return code: {exec_result.get('return_code')}") + print(f"Execution time: {exec_result.get('execution_time_ms')}ms") + + if exec_result.get("success"): + print("Status: ✅ SUCCESS") + else: + print("Status: ❌ FAILED") + + if exec_result.get("stdout"): + print(f"\nStdout:\n{exec_result['stdout']}") + if exec_result.get("stderr"): + print(f"\nStderr:\n{exec_result['stderr']}") + + print() + + sys.exit(0 if results["valid"] else 1) + + except BettyError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + print("\nInterrupted", file=sys.stderr) + sys.exit(130) + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/hook.simulate/skill.yaml b/skills/hook.simulate/skill.yaml new file mode 100644 index 0000000..b1f377b --- /dev/null +++ b/skills/hook.simulate/skill.yaml @@ -0,0 +1,50 @@ +name: hook.simulate +version: 0.1.0 +description: "Simulate hook execution to test manifests before registration" + +inputs: + - name: manifest_path + type: string + required: true + description: "Path to the hook manifest file (YAML)" + + - name: execute + type: boolean + required: false + default: false + description: "Whether to execute the hook command (vs. just validation)" + + - name: dry_run + type: boolean + required: false + default: true + description: "If true, simulate command execution without running it" + +outputs: + - name: simulation_result + type: object + description: "Complete simulation results including validation, triggers, and execution" + schema: + properties: + valid: boolean + validation_errors: array + manifest: object + trigger_simulation: object + command_executions: array + blocking: boolean + timeout_ms: number + +dependencies: + - None + +entrypoints: + - command: /skill/hook/simulate + handler: hook_simulate.py + runtime: python + permissions: + - filesystem:read + - filesystem:write + +status: active + +tags: [hook, simulation, testing, validation, development] diff --git a/skills/meta.compatibility/README.md b/skills/meta.compatibility/README.md new file mode 100644 index 0000000..cf3b579 --- /dev/null +++ b/skills/meta.compatibility/README.md @@ -0,0 +1,239 @@ +# meta.compatibility + +Automatic artifact dependency graph validation and diagnostics for the Betty Framework. + +## Overview + +The `meta.compatibility` skill analyzes the artifact dependency graph across all Betty skills, detecting compatibility issues, cycles, orphan nodes, and unresolved dependencies. It provides actionable diagnostics to maintain a healthy skill ecosystem. + +## Features + +- **Graph Construction**: Builds directed graph from skill artifact metadata +- **Cycle Detection**: Identifies recursive dependencies (marks as `recursive:true`) +- **Orphan Detection**: Finds isolated nodes with no connections +- **Dependency Analysis**: Detects unresolved dependencies (consumed but never produced) +- **Producer Analysis**: Identifies orphan producers (produced but never consumed) +- **Health Reporting**: Comprehensive JSON and human-readable reports + +## Usage + +### Basic Validation + +```bash +betty run /meta/compatibility --check artifacts +``` + +### Save Report to File + +```bash +betty run /meta/compatibility --check artifacts --output graph-report.json +``` + +### JSON Output Only + +```bash +betty run /meta/compatibility --check artifacts --json +``` + +### Custom Skills Directory + +```bash +betty run /meta/compatibility --check artifacts --skills-dir /path/to/skills +``` + +## Output Format + +### Human-Readable Output + +``` +====================================================================== +Artifact Dependency Graph Validation +====================================================================== +Total Skills: 25 +Total Artifacts: 42 +Graph Density: 12.50% + +✅ 38 artifacts connected +⚠️ 3 isolated + - report.template + - legacy.format + - experimental.data +❌ 1 cycle(s) detected + 1. model-a → model-b → model-a + +✅ Graph Status: HEALTHY +====================================================================== +``` + +### JSON Report + +```json +{ + "total_artifacts": 42, + "total_skills": 25, + "connected": 38, + "isolated": ["report.template", "legacy.format"], + "cyclic": [["model-a", "model-b", "model-a"]], + "unresolved": ["dataset.missing"], + "orphans": ["deprecated.artifact"], + "status": "warning", + "graph_stats": { + "nodes": 42, + "edges": 58, + "density": 0.125 + } +} +``` + +## Report Fields + +| Field | Type | Description | +|-------|------|-------------| +| `total_artifacts` | number | Total artifact types in the graph | +| `total_skills` | number | Total skills with artifact metadata | +| `connected` | number | Artifacts with at least one connection | +| `isolated` | array | Artifact types with no connections | +| `cyclic` | array | Lists of artifact cycles (recursive dependencies) | +| `unresolved` | array | Artifacts consumed but never produced | +| `orphans` | array | Artifacts produced but never consumed | +| `status` | string | Overall health: `healthy`, `warning`, or `error` | +| `graph_stats` | object | Graph metrics (nodes, edges, density) | + +## Status Levels + +- **healthy**: No critical issues detected +- **warning**: Non-critical issues (isolated nodes, orphan producers) +- **error**: Critical issues (cycles, unresolved dependencies) + +## Validation Rules + +### Critical Issues (Error Status) + +1. **Cycles**: Circular dependencies in artifact flow + - Example: Skill A produces X, consumes Y; Skill B produces Y, consumes X + - Can cause infinite loops in workflows + +2. **Unresolved Dependencies**: Artifacts consumed but never produced + - Example: Skill requires `api-spec` but no skill produces it + - Breaks skill composition and workflows + +### Warnings (Warning Status) + +1. **Isolated Nodes**: Artifacts with no producers or consumers + - May indicate deprecated or unused artifact types + - Not critical but suggests cleanup needed + +2. **Orphan Producers**: Artifacts produced but never consumed + - May indicate missing consumer skills + - Not critical but suggests incomplete workflows + +## Example Scenarios + +### Scenario 1: Healthy Graph + +``` +Skills: + - api.define: produces openapi-spec + - api.validate: consumes openapi-spec, produces validation-report + - api.test: consumes openapi-spec, produces test-results + +Result: ✅ All connected, no issues +``` + +### Scenario 2: Cycle Detected + +``` +Skills: + - transform.a: consumes data-raw, produces data-interim + - transform.b: consumes data-interim, produces data-processed + - transform.c: consumes data-processed, produces data-raw + +Result: ❌ Cycle detected: data-raw → data-interim → data-processed → data-raw +``` + +### Scenario 3: Unresolved Dependency + +``` +Skills: + - api.test: consumes api-spec + - (no skill produces api-spec) + +Result: ❌ Unresolved dependency: api-spec consumed but never produced +``` + +### Scenario 4: Isolated Artifact + +``` +Skills: + - legacy.export: produces legacy-format + - (no skill consumes legacy-format) + - (no skill produces legacy-format to feed others) + +Result: ⚠️ Isolated: legacy-format (if also not consumed, orphan producer) +``` + +## Implementation Details + +### Graph Construction + +The skill builds a directed graph where: +- **Nodes**: Artifact types (e.g., `openapi-spec`, `validation-report`) +- **Edges**: Dependency flows (A → B means artifact A is consumed by a skill that produces B) +- **Attributes**: Each node tracks its producers and consumers (skill names) + +```python +import networkx as nx + +def build_artifact_graph(artifacts): + g = nx.DiGraph() + for artifact in artifacts: + g.add_node(artifact["id"]) + for dep in artifact.get("consumes", []): + g.add_edge(dep, artifact["id"]) + return g +``` + +### Validation Algorithms + +1. **Cycle Detection**: Uses `networkx.simple_cycles()` to find all cycles +2. **Isolated Nodes**: Checks `in_degree == 0 and out_degree == 0` +3. **Unresolved**: Nodes with consumers but no producers +4. **Orphans**: Nodes with producers but no consumers + +## Integration + +This skill is part of the meta-skills layer and can be used to: + +1. **Pre-flight Checks**: Validate graph before deploying new skills +2. **CI/CD**: Run in pipelines to ensure artifact compatibility +3. **Documentation**: Generate artifact flow diagrams +4. **Debugging**: Identify why skill compositions fail + +## Dependencies + +- `networkx>=3.0`: Graph analysis library +- `PyYAML>=6.0`: Parse skill.yaml files + +## Related Skills + +- `artifact.validate`: Validates individual artifact files +- `artifact.define`: Defines artifact metadata schemas +- `registry.query`: Queries skill registry for artifact information + +## Exit Codes + +- `0`: Success (healthy or warning status) +- `1`: Error (critical issues detected) + +## Limitations + +- Does not validate actual artifact files, only metadata +- Wildcard consumers (`type: "*"`) are ignored in graph construction +- Does not check runtime compatibility, only structural compatibility + +## Future Enhancements + +- Visual graph rendering (GraphViz output) +- Suggested fixes for detected issues +- Artifact version compatibility checking +- Runtime dependency validation diff --git a/skills/meta.compatibility/__init__.py b/skills/meta.compatibility/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/meta.compatibility/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/meta.compatibility/meta_compatibility.py b/skills/meta.compatibility/meta_compatibility.py new file mode 100755 index 0000000..d5daecd --- /dev/null +++ b/skills/meta.compatibility/meta_compatibility.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +""" +meta.compatibility skill - Automatic artifact dependency graph validation + +Validates artifact dependency graphs, detects cycles, orphan nodes, and unresolved +dependencies. Provides actionable diagnostics for artifact compatibility issues. +""" + +import sys +import os +import argparse +import json +from pathlib import Path +from typing import Dict, Any, List, Set, Tuple, Optional +import yaml + +try: + import networkx as nx +except ImportError: + print("Error: networkx is required. Install with: pip install networkx>=3.0") + sys.exit(1) + + + +def load_skill_metadata(skills_dir: Path) -> List[Dict[str, Any]]: + """Load artifact metadata from all skills""" + skills = [] + + if not skills_dir.exists(): + return skills + + for skill_path in skills_dir.iterdir(): + if not skill_path.is_dir() or skill_path.name.startswith('.'): + continue + + # Try to load skill.yaml + skill_yaml = skill_path / "skill.yaml" + if not skill_yaml.exists(): + continue + + try: + with open(skill_yaml, 'r') as f: + skill_data = yaml.safe_load(f) + + if not skill_data: + continue + + # Extract artifact metadata + if 'artifact_metadata' in skill_data: + skills.append({ + 'name': skill_data.get('name', skill_path.name), + 'path': str(skill_path), + 'produces': skill_data['artifact_metadata'].get('produces', []), + 'consumes': skill_data['artifact_metadata'].get('consumes', []) + }) + except Exception as e: + print(f"Warning: Failed to load {skill_yaml}: {e}", file=sys.stderr) + continue + + return skills + + +def build_artifact_graph(artifacts: List[Dict[str, Any]]) -> nx.DiGraph: + """ + Build artifact dependency graph from skill metadata. + + Nodes represent artifact types. + Edges represent dependencies: if skill A produces artifact X and skill B consumes X, + there's an edge from X (as produced by A) to X (as consumed by B). + + We also track which skills produce/consume each artifact type. + """ + g = nx.DiGraph() + + # Track artifact types and their producers/consumers + artifact_types: Set[str] = set() + producers: Dict[str, List[str]] = {} # artifact_type -> [skill_names] + consumers: Dict[str, List[str]] = {} # artifact_type -> [skill_names] + + # First pass: collect all artifact types and track producers/consumers + for skill in artifacts: + skill_name = skill.get('name', 'unknown') + + # Process produces + for artifact in skill.get('produces', []): + if isinstance(artifact, dict): + artifact_type = artifact.get('type') + else: + artifact_type = artifact + + if artifact_type: + artifact_types.add(artifact_type) + if artifact_type not in producers: + producers[artifact_type] = [] + producers[artifact_type].append(skill_name) + + # Process consumes + for artifact in skill.get('consumes', []): + if isinstance(artifact, dict): + artifact_type = artifact.get('type') + else: + artifact_type = artifact + + if artifact_type and artifact_type != '*': # Ignore wildcard consumers + artifact_types.add(artifact_type) + if artifact_type not in consumers: + consumers[artifact_type] = [] + consumers[artifact_type].append(skill_name) + + # Add nodes for each artifact type + for artifact_type in artifact_types: + g.add_node( + artifact_type, + producers=producers.get(artifact_type, []), + consumers=consumers.get(artifact_type, []) + ) + + # Add edges representing artifact flows + # If artifact A is consumed by a skill that produces artifact B, + # create edge A -> B (artifact A flows into creating artifact B) + for skill in artifacts: + consumed_artifacts = [] + produced_artifacts = [] + + # Get consumed artifact types + for artifact in skill.get('consumes', []): + if isinstance(artifact, dict): + artifact_type = artifact.get('type') + else: + artifact_type = artifact + + if artifact_type and artifact_type != '*': + consumed_artifacts.append(artifact_type) + + # Get produced artifact types + for artifact in skill.get('produces', []): + if isinstance(artifact, dict): + artifact_type = artifact.get('type') + else: + artifact_type = artifact + + if artifact_type: + produced_artifacts.append(artifact_type) + + # Create edges from consumed to produced artifacts + for consumed in consumed_artifacts: + for produced in produced_artifacts: + if consumed in g and produced in g: + g.add_edge(consumed, produced, skill=skill.get('name')) + + return g + + +def detect_cycles(g: nx.DiGraph) -> List[List[str]]: + """Detect cycles in the artifact dependency graph""" + try: + cycles = list(nx.simple_cycles(g)) + return cycles + except Exception: + return [] + + +def identify_isolated_nodes(g: nx.DiGraph) -> List[str]: + """Identify nodes with no incoming or outgoing edges""" + isolated = [] + for node in g.nodes(): + if g.in_degree(node) == 0 and g.out_degree(node) == 0: + isolated.append(node) + return isolated + + +def identify_unresolved_dependencies(g: nx.DiGraph, skills: List[Dict[str, Any]]) -> List[str]: + """Identify artifact types that are consumed but never produced""" + unresolved = [] + + for node in g.nodes(): + node_data = g.nodes[node] + producers = node_data.get('producers', []) + consumers = node_data.get('consumers', []) + + # If consumed but not produced, it's unresolved + if consumers and not producers: + unresolved.append(node) + + return unresolved + + +def identify_orphan_producers(g: nx.DiGraph) -> List[str]: + """Identify artifact types that are produced but never consumed""" + orphans = [] + + for node in g.nodes(): + node_data = g.nodes[node] + producers = node_data.get('producers', []) + consumers = node_data.get('consumers', []) + + # If produced but not consumed, it's an orphan + if producers and not consumers: + orphans.append(node) + + return orphans + + +def validate_artifact_graph(skills_dir: str = "skills") -> Dict[str, Any]: + """ + Validate artifact dependency graph and generate diagnostic report + + Args: + skills_dir: Path to skills directory + + Returns: + Validation report with graph health metrics + """ + skills_path = Path(skills_dir) + + # Load skill metadata + skills = load_skill_metadata(skills_path) + + if not skills: + return { + 'success': False, + 'error': 'No skills with artifact metadata found', + 'total_artifacts': 0, + 'total_skills': 0, + 'status': 'error' + } + + # Build graph + g = build_artifact_graph(skills) + + # Detect issues + cycles = detect_cycles(g) + isolated = identify_isolated_nodes(g) + unresolved = identify_unresolved_dependencies(g, skills) + orphans = identify_orphan_producers(g) + + # Calculate connected artifacts (not isolated) + connected = len(g.nodes()) - len(isolated) + + # Determine status + status = 'healthy' + if cycles or unresolved: + status = 'error' + elif isolated or orphans: + status = 'warning' + + # Build report + report = { + 'success': True, + 'total_artifacts': len(g.nodes()), + 'total_skills': len(skills), + 'connected': connected, + 'isolated': isolated, + 'cyclic': [list(cycle) for cycle in cycles], + 'unresolved': unresolved, + 'orphans': orphans, + 'status': status, + 'graph_stats': { + 'nodes': g.number_of_nodes(), + 'edges': g.number_of_edges(), + 'density': nx.density(g) if g.number_of_nodes() > 0 else 0 + } + } + + return report + + +def print_human_readable_report(report: Dict[str, Any]) -> None: + """Print human-readable graph validation summary""" + if not report.get('success'): + print(f"\n❌ Graph Validation Failed") + print(f"Error: {report.get('error', 'Unknown error')}") + return + + total = report.get('total_artifacts', 0) + connected = report.get('connected', 0) + isolated = report.get('isolated', []) + cycles = report.get('cyclic', []) + unresolved = report.get('unresolved', []) + orphans = report.get('orphans', []) + status = report.get('status', 'unknown') + + print(f"\n{'='*70}") + print(f"Artifact Dependency Graph Validation") + print(f"{'='*70}") + print(f"Total Skills: {report.get('total_skills', 0)}") + print(f"Total Artifacts: {total}") + print(f"Graph Density: {report['graph_stats']['density']:.2%}") + print(f"") + + # Connected artifacts + if connected > 0: + print(f"✅ {connected} artifacts connected") + + # Isolated artifacts + if isolated: + print(f"⚠️ {len(isolated)} isolated") + for artifact in isolated[:5]: # Show first 5 + print(f" - {artifact}") + if len(isolated) > 5: + print(f" ... and {len(isolated) - 5} more") + + # Cycles + if cycles: + print(f"❌ {len(cycles)} cycle(s) detected") + for i, cycle in enumerate(cycles[:3], 1): # Show first 3 + cycle_path = ' → '.join(cycle + [cycle[0]]) + print(f" {i}. {cycle_path}") + if len(cycles) > 3: + print(f" ... and {len(cycles) - 3} more") + + # Unresolved dependencies + if unresolved: + print(f"❌ {len(unresolved)} unresolved dependencies") + for artifact in unresolved[:5]: + print(f" - {artifact} (consumed but never produced)") + if len(unresolved) > 5: + print(f" ... and {len(unresolved) - 5} more") + + # Orphan producers + if orphans: + print(f"⚠️ {len(orphans)} orphan producers") + for artifact in orphans[:5]: + print(f" - {artifact} (produced but never consumed)") + if len(orphans) > 5: + print(f" ... and {len(orphans) - 5} more") + + print(f"") + + # Overall status + if status == 'healthy': + print(f"✅ Graph Status: HEALTHY") + elif status == 'warning': + print(f"⚠️ Graph Status: WARNING (non-critical issues)") + else: + print(f"❌ Graph Status: ERROR (critical issues)") + + print(f"{'='*70}\n") + + +def main(): + """Main entry point for meta.compatibility skill""" + parser = argparse.ArgumentParser( + description='Validate artifact dependency graph and detect compatibility issues' + ) + parser.add_argument( + '--check', + type=str, + choices=['artifacts', 'skills', 'all'], + default='artifacts', + help='What to check (default: artifacts)' + ) + parser.add_argument( + '--skills-dir', + type=str, + default='skills', + help='Path to skills directory (default: skills)' + ) + parser.add_argument( + '--output', + type=str, + help='Save JSON report to file' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output JSON only (no human-readable output)' + ) + + args = parser.parse_args() + + # Validate artifact graph + report = validate_artifact_graph(skills_dir=args.skills_dir) + + # Save to file if requested + if args.output: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'w') as f: + json.dump(report, f, indent=2) + if not args.json: + print(f"Report saved to: {output_path}") + + # Print output + if args.json: + print(json.dumps(report, indent=2)) + else: + print_human_readable_report(report) + + # Exit with appropriate code + status = report.get('status', 'error') + if status == 'healthy': + return 0 + elif status == 'warning': + return 0 # Warnings don't fail the check + else: + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/skills/meta.compatibility/skill.yaml b/skills/meta.compatibility/skill.yaml new file mode 100644 index 0000000..a8490b7 --- /dev/null +++ b/skills/meta.compatibility/skill.yaml @@ -0,0 +1,107 @@ +name: meta.compatibility +version: 0.1.0 +description: > + Automatic artifact dependency graph validation and diagnostics. Builds a dependency + graph from skill artifact metadata, detects cycles, orphan nodes, unresolved dependencies, + and provides actionable health reports for the Betty Framework ecosystem. + +inputs: + - name: check + type: string + required: false + default: artifacts + description: What to check (artifacts, skills, or all) + + - name: skills_dir + type: string + required: false + default: skills + description: Path to skills directory + + - name: output + type: string + required: false + description: Save JSON report to file + + - name: json + type: boolean + required: false + default: false + description: Output JSON only (no human-readable format) + +outputs: + - name: graph_report + type: object + description: > + Comprehensive graph validation report including total artifacts, connected nodes, + isolated nodes, cycles, unresolved dependencies, and overall health status + + - name: status + type: string + description: Overall graph health status (healthy, warning, or error) + + - name: total_artifacts + type: number + description: Total number of artifact types in the graph + +dependencies: [] + +entrypoints: + - command: /meta/compatibility + handler: meta_compatibility.py + runtime: python + description: > + Validate artifact dependency graph across all Betty skills. Constructs a directed + graph where nodes represent artifact types and edges represent producer-consumer + relationships. Detects cycles (recursive dependencies), isolated nodes (disconnected + artifacts), orphan producers (artifacts produced but never consumed), and unresolved + dependencies (artifacts consumed but never produced). Generates comprehensive + health reports with actionable diagnostics. + parameters: + - name: check + type: string + required: false + default: artifacts + description: What to check + - name: skills_dir + type: string + required: false + default: skills + description: Skills directory path + - name: output + type: string + required: false + description: Output file for JSON report + - name: json + type: boolean + required: false + default: false + description: JSON-only output + permissions: + - filesystem:read + +status: active + +tags: + - meta + - validation + - artifacts + - compatibility + - graph + - diagnostics + - tier2 + - phase3 + +# This skill's own artifact metadata +artifact_metadata: + produces: + - type: compatibility-report + description: Artifact dependency graph validation report with health metrics and diagnostics + file_pattern: "*.compatibility-report.json" + content_type: application/json + + consumes: + - type: skill-metadata + description: Reads artifact_metadata from all skill.yaml files + file_pattern: "**/skill.yaml" + required: true diff --git a/skills/plugin.build/SKILL.md b/skills/plugin.build/SKILL.md new file mode 100644 index 0000000..961bcc2 --- /dev/null +++ b/skills/plugin.build/SKILL.md @@ -0,0 +1,732 @@ +--- +name: Plugin Build +description: Bundle plugin directory into a deployable Claude Code plugin package +--- + +# plugin.build + +## Overview + +**plugin.build** is the packaging tool that bundles your Betty Framework plugin into a distributable archive ready for deployment to Claude Code. It validates all entrypoints, gathers necessary files, and creates versioned packages with checksums. + +## Purpose + +Automates the creation of deployable plugin packages by: +- **Validating** all declared entrypoints and handler files +- **Gathering** all necessary plugin files (skills, utilities, registries) +- **Packaging** into `.tar.gz` or `.zip` archives +- **Generating** checksums for package verification +- **Reporting** validation results and build metrics + +This eliminates manual packaging errors and ensures consistent, reproducible plugin distributions. + +## What It Does + +1. **Loads plugin.yaml**: Reads the plugin configuration +2. **Validates Entrypoints**: Checks that all command handlers exist on disk +3. **Gathers Files**: Collects skills, utilities, registries, and documentation +4. **Creates Package**: Bundles everything into a versioned archive +5. **Calculates Checksums**: Generates MD5 and SHA256 hashes +6. **Generates Manifest**: Creates manifest.json with entrypoint summary and checksums +7. **Creates Preview**: Generates plugin.preview.yaml for review before deployment +8. **Generates Report**: Outputs detailed build metrics as JSON +9. **Reports Issues**: Identifies missing files or validation errors + +## Usage + +### Basic Usage + +```bash +python skills/plugin.build/plugin_build.py +``` + +Builds with defaults: +- Plugin: `./plugin.yaml` +- Format: `tar.gz` +- Output: `./dist/` + +### Via Betty CLI + +```bash +/plugin/build +``` + +### Custom Plugin Path + +```bash +python skills/plugin.build/plugin_build.py /path/to/plugin.yaml +``` + +### Specify Output Format + +```bash +python skills/plugin.build/plugin_build.py --format=zip +``` + +```bash +python skills/plugin.build/plugin_build.py --format=tar.gz +``` + +### Custom Output Directory + +```bash +python skills/plugin.build/plugin_build.py --output-dir=/tmp/packages +``` + +### Full Options + +```bash +python skills/plugin.build/plugin_build.py \ + /custom/path/plugin.yaml \ + --format=zip \ + --output-dir=/var/packages +``` + +## Command-Line Arguments + +| Argument | Type | Default | Description | +|----------|------|---------|-------------| +| `plugin_path` | Positional | `./plugin.yaml` | Path to plugin.yaml file | +| `--format` | Option | `tar.gz` | Package format (tar.gz or zip) | +| `--output-dir` | Option | `./dist` | Output directory for packages | + +## Output Files + +### Package Archive + +**Naming convention**: `{plugin-name}-{version}.{format}` + +Examples: +- `betty-framework-1.0.0.tar.gz` +- `betty-framework-1.0.0.zip` + +**Location**: `{output-dir}/{package-name}.{format}` + +### Manifest File + +**Naming convention**: `manifest.json` + +**Location**: `{output-dir}/manifest.json` + +Contains plugin metadata, entrypoint summary, and package checksums. This is the primary file for plugin distribution and installation. + +### Plugin Preview + +**Naming convention**: `plugin.preview.yaml` + +**Location**: `{output-dir}/plugin.preview.yaml` + +Contains the current plugin configuration for review before deployment. Useful for comparing changes or validating the plugin structure. + +### Build Report + +**Naming convention**: `{plugin-name}-{version}-build-report.json` + +Example: +- `betty-framework-1.0.0-build-report.json` + +**Location**: `{output-dir}/{package-name}-build-report.json` + +## Package Structure + +The generated archive contains: + +``` +betty-framework-1.0.0/ +├── plugin.yaml # Plugin manifest +├── requirements.txt # Python dependencies +├── README.md # Documentation (if exists) +├── LICENSE # License file (if exists) +├── CHANGELOG.md # Change log (if exists) +├── betty/ # Core utility package +│ ├── __init__.py +│ ├── config.py +│ ├── validation.py +│ ├── logging_utils.py +│ ├── file_utils.py +│ └── errors.py +├── registry/ # Registry files +│ ├── skills.json +│ ├── commands.json +│ ├── hooks.json +│ └── agents.json +└── skills/ # All active skills + ├── api.define/ + │ ├── skill.yaml + │ ├── SKILL.md + │ └── api_define.py + ├── api.validate/ + │ ├── skill.yaml + │ ├── SKILL.md + │ └── api_validate.py + └── ... (all other skills) +``` + +## Manifest Schema + +The `manifest.json` file is the primary metadata file for plugin distribution: + +```json +{ + "name": "betty-framework", + "version": "1.0.0", + "description": "Betty Framework - Structured AI-assisted engineering", + "author": { + "name": "RiskExec", + "email": "platform@riskexec.com", + "url": "https://github.com/epieczko/betty" + }, + "license": "MIT", + "metadata": { + "homepage": "https://github.com/epieczko/betty", + "repository": "https://github.com/epieczko/betty", + "documentation": "https://github.com/epieczko/betty/tree/main/docs", + "tags": ["framework", "api-development", "workflow"], + "generated_at": "2025-10-23T12:34:56.789012+00:00" + }, + "requirements": { + "python": ">=3.11", + "packages": ["pyyaml"] + }, + "permissions": ["filesystem:read", "filesystem:write", "process:execute"], + "package": { + "filename": "betty-framework-1.0.0.tar.gz", + "size_bytes": 245760, + "checksums": { + "md5": "a1b2c3d4e5f6...", + "sha256": "1234567890abcdef..." + } + }, + "entrypoints": [ + { + "command": "skill/define", + "handler": "skills/skill.define/skill_define.py", + "runtime": "python" + } + ], + "commands_count": 18, + "agents": [ + { + "name": "api.designer", + "description": "Design APIs with iterative refinement" + } + ] +} +``` + +## Build Report Schema + +```json +{ + "build_timestamp": "2025-10-23T12:34:56.789012+00:00", + "plugin": { + "name": "betty-framework", + "version": "1.0.0", + "description": "Betty Framework - Structured AI-assisted engineering" + }, + "validation": { + "total_commands": 18, + "valid_entrypoints": 18, + "missing_files": [], + "has_errors": false + }, + "package": { + "path": "/home/user/betty/dist/betty-framework-1.0.0.tar.gz", + "size_bytes": 245760, + "size_human": "240.00 KB", + "files_count": 127, + "format": "tar.gz", + "checksums": { + "md5": "a1b2c3d4e5f6...", + "sha256": "1234567890abcdef..." + } + }, + "entrypoints": [ + { + "command": "skill/define", + "handler": "skills/skill.define/skill_define.py", + "runtime": "python", + "path": "/home/user/betty/skills/skill.define/skill_define.py" + } + ] +} +``` + +## Outputs + +### Success Response + +```json +{ + "ok": true, + "status": "success", + "package_path": "/home/user/betty/dist/betty-framework-1.0.0.tar.gz", + "report_path": "/home/user/betty/dist/betty-framework-1.0.0-build-report.json", + "build_report": { ... } +} +``` + +### Success with Warnings + +```json +{ + "ok": false, + "status": "success_with_warnings", + "package_path": "/home/user/betty/dist/betty-framework-1.0.0.tar.gz", + "report_path": "/home/user/betty/dist/betty-framework-1.0.0-build-report.json", + "build_report": { + "validation": { + "missing_files": [ + "Command 'api.broken': handler not found at skills/api.broken/api_broken.py" + ], + "has_errors": true + } + } +} +``` + +### Failure Response + +```json +{ + "ok": false, + "status": "failed", + "error": "plugin.yaml not found: /home/user/betty/plugin.yaml" +} +``` + +## Behavior + +### 1. Plugin Loading + +Reads and parses `plugin.yaml`: +- Validates YAML syntax +- Extracts plugin name, version, description +- Identifies all command entrypoints + +### 2. Entrypoint Validation + +For each command in `plugin.yaml`: +- Extracts handler script path +- Checks file existence on disk +- Reports valid and missing handlers +- Logs validation results + +**Valid entrypoint**: +```yaml +- name: skill/validate + handler: + runtime: python + script: skills/skill.define/skill_define.py +``` + +**Missing handler** (reports warning): +```yaml +- name: broken/command + handler: + runtime: python + script: skills/broken/missing.py # File doesn't exist +``` + +### 3. File Gathering + +Automatically includes: + +**Always included**: +- `plugin.yaml` – Plugin manifest +- `skills/*/` – All skill directories referenced in commands +- `betty/` – Core utility package +- `registry/*.json` – All registry files + +**Conditionally included** (if exist): +- `requirements.txt` – Python dependencies +- `README.md` – Plugin documentation +- `LICENSE` – License file +- `CHANGELOG.md` – Version history + +**Excluded**: +- `__pycache__/` directories +- `.pyc` compiled Python files +- Hidden files (starting with `.`) +- Build artifacts and temporary files + +### 4. Package Creation + +**tar.gz format**: +- GZIP compression +- Preserves file permissions +- Cross-platform compatible +- Standard for Python packages + +**zip format**: +- ZIP compression +- Wide compatibility +- Good for Windows environments +- Easy to extract without CLI tools + +### 5. Checksum Generation + +Calculates two checksums: + +**MD5**: Fast, widely supported +``` +a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 +``` + +**SHA256**: Cryptographically secure +``` +1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +``` + +Use checksums to verify package integrity after download or transfer. + +## Examples + +### Example 1: Standard Build + +**Scenario**: Build Betty Framework plugin for distribution + +```bash +# Ensure plugin.yaml is up to date +/plugin/sync + +# Build the package +/plugin/build +``` + +**Output**: +``` +INFO: 🏗️ Starting plugin build... +INFO: 📄 Plugin: /home/user/betty/plugin.yaml +INFO: 📦 Format: tar.gz +INFO: 📁 Output: /home/user/betty/dist +INFO: ✅ Loaded plugin.yaml from /home/user/betty/plugin.yaml +INFO: 📝 Validating 18 command entrypoints... +INFO: 📦 Gathering files from 16 skill directories... +INFO: 📦 Adding betty/ utility package... +INFO: 📦 Adding registry/ files... +INFO: 📦 Total files to package: 127 +INFO: 🗜️ Creating tar.gz archive: /home/user/betty/dist/betty-framework-1.0.0.tar.gz +INFO: 🔐 Calculating checksums... +INFO: MD5: a1b2c3d4e5f6... +INFO: SHA256: 1234567890ab... +INFO: 📊 Build report: /home/user/betty/dist/betty-framework-1.0.0-build-report.json +INFO: 📋 Manifest: /home/user/betty/dist/manifest.json +INFO: 📋 Preview file created: /home/user/betty/dist/plugin.preview.yaml +INFO: +============================================================ +INFO: 🎉 BUILD COMPLETE +============================================================ +INFO: 📦 Package: /home/user/betty/dist/betty-framework-1.0.0.tar.gz +INFO: 📊 Report: /home/user/betty/dist/betty-framework-1.0.0-build-report.json +INFO: 📋 Manifest: /home/user/betty/dist/manifest.json +INFO: 👁️ Preview: /home/user/betty/dist/plugin.preview.yaml +INFO: ✅ Commands: 18/18 +INFO: 📏 Size: 240.00 KB +INFO: 📝 Files: 127 +============================================================ +``` + +### Example 2: Build as ZIP + +**Scenario**: Create Windows-friendly package + +```bash +python skills/plugin.build/plugin_build.py --format=zip +``` + +**Result**: `dist/betty-framework-1.0.0.zip` + +### Example 3: Build with Custom Output + +**Scenario**: Build to specific release directory + +```bash +python skills/plugin.build/plugin_build.py \ + --format=tar.gz \ + --output-dir=releases/v1.0.0 +``` + +**Result**: +- `releases/v1.0.0/betty-framework-1.0.0.tar.gz` +- `releases/v1.0.0/betty-framework-1.0.0-build-report.json` + +### Example 4: Detecting Missing Handlers + +**Scenario**: Some handlers are missing + +```bash +# Remove a handler file +rm skills/api.validate/api_validate.py + +# Try to build +/plugin/build +``` + +**Output**: +``` +INFO: 📝 Validating 18 command entrypoints... +WARNING: ❌ skill/api/validate: skills/api.validate/api_validate.py (not found) +INFO: ⚠️ Found 1 missing files: +INFO: - Command 'skill/api/validate': handler not found at skills/api.validate/api_validate.py +INFO: 📦 Gathering files from 15 skill directories... +... +INFO: ============================================================ +INFO: 🎉 BUILD COMPLETE +INFO: ============================================================ +INFO: ✅ Commands: 17/18 +INFO: ⚠️ Warnings: 1 +``` + +**Exit code**: 1 (failure due to validation errors) + +### Example 5: Build Workflow + +**Scenario**: Complete release workflow + +```bash +# 1. Update registries +/registry/update + +# 2. Sync plugin.yaml +/plugin/sync + +# 3. Build package +/plugin/build + +# 4. Verify package +tar -tzf dist/betty-framework-1.0.0.tar.gz | head -20 + +# 5. Check build report +cat dist/betty-framework-1.0.0-build-report.json | jq . +``` + +## Integration + +### With plugin.sync + +Always sync before building: + +```bash +/plugin/sync && /plugin/build +``` + +### With Workflows + +Include in release workflow: + +```yaml +# workflows/plugin_release.yaml +name: plugin_release +version: 1.0.0 +description: Release workflow for Betty Framework plugin + +steps: + - skill: registry.update + description: Update all registries + + - skill: plugin.sync + description: Generate plugin.yaml + + - skill: plugin.build + args: ["--format=tar.gz"] + description: Build tar.gz package + + - skill: plugin.build + args: ["--format=zip"] + description: Build zip package +``` + +### With CI/CD + +Add to GitHub Actions: + +```yaml +# .github/workflows/release.yml +- name: Build Plugin Package + run: | + python skills/plugin.sync/plugin_sync.py + python skills/plugin.build/plugin_build.py --format=tar.gz + python skills/plugin.build/plugin_build.py --format=zip + +- name: Upload Packages + uses: actions/upload-artifact@v3 + with: + name: plugin-packages + path: dist/betty-framework-* +``` + +## Validation Rules + +### Entrypoint Validation + +**Valid entrypoint**: +- Command name is defined +- Handler section exists +- Handler script path is specified +- Handler file exists on disk +- Runtime is specified + +**Invalid entrypoint** (triggers warning): +- Missing handler script path +- Handler file doesn't exist +- Empty command name + +### File Gathering Rules + +**Skill directories included if**: +- Referenced in at least one command handler +- Contains valid handler file + +**Files excluded**: +- Python cache files (`__pycache__`, `.pyc`) +- Hidden files (`.git`, `.env`, etc.) +- Build artifacts (`dist/`, `build/`) +- IDE files (`.vscode/`, `.idea/`) + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "plugin.yaml not found" | Missing plugin.yaml | Run `/plugin/sync` first | +| "Invalid YAML" | Syntax error in plugin.yaml | Fix YAML syntax | +| "Unsupported output format" | Invalid --format value | Use `tar.gz` or `zip` | +| "Handler not found" | Missing handler file | Create handler or fix path | +| "Permission denied" | Cannot write to output dir | Check directory permissions | + +## Files Read + +- `plugin.yaml` – Plugin manifest +- `skills/*/skill.yaml` – Skill manifests (indirect via handlers) +- `skills/*/*.py` – Handler scripts +- `betty/*.py` – Utility modules +- `registry/*.json` – Registry files +- `requirements.txt` – Dependencies (if exists) +- `README.md`, `LICENSE`, `CHANGELOG.md` – Documentation (if exist) + +## Files Modified + +- `{output-dir}/{plugin-name}-{version}.{format}` – Created package archive +- `{output-dir}/{plugin-name}-{version}-build-report.json` – Created build report +- `{output-dir}/manifest.json` – Created plugin manifest with checksums and entrypoints +- `{output-dir}/plugin.preview.yaml` – Created plugin preview for review + +## Exit Codes + +- **0**: Success (all handlers valid, package created) +- **1**: Failure (missing handlers or build error) + +## Logging + +Logs build progress with emojis for clarity: + +``` +INFO: 🏗️ Starting plugin build... +INFO: 📄 Plugin: /home/user/betty/plugin.yaml +INFO: ✅ Loaded plugin.yaml +INFO: 📝 Validating entrypoints... +INFO: 📦 Gathering files... +INFO: 🗜️ Creating tar.gz archive... +INFO: 🔐 Calculating checksums... +INFO: 📊 Build report written +INFO: 🎉 BUILD COMPLETE +``` + +## Best Practices + +1. **Always Sync First**: Run `/plugin/sync` before `/plugin/build` +2. **Validate Before Building**: Ensure all skills are registered and active +3. **Check Build Reports**: Review validation warnings before distribution +4. **Verify Checksums**: Use checksums to verify package integrity +5. **Version Consistently**: Match plugin version with git tags +6. **Test Extraction**: Verify packages extract correctly +7. **Document Changes**: Keep CHANGELOG.md updated + +## Troubleshooting + +### Missing Files in Package + +**Problem**: Expected files not in archive + +**Solutions**: +- Ensure files exist in source directory +- Check that skills are referenced in commands +- Verify files aren't excluded (e.g., `.pyc`, `__pycache__`) +- Check file permissions + +### Handler Validation Failures + +**Problem**: Handlers marked as missing but files exist + +**Solutions**: +- Verify exact path in plugin.yaml matches file location +- Check case sensitivity in file paths +- Ensure handler files have correct names +- Run `/plugin/sync` to update paths + +### Package Size Too Large + +**Problem**: Archive file is very large + +**Solutions**: +- Remove unused skills from plugin.yaml +- Check for accidentally included large files +- Review skill directories for unnecessary data +- Consider splitting into multiple plugins + +### Build Fails with Permission Error + +**Problem**: Cannot write to output directory + +**Solutions**: +- Create output directory with proper permissions +- Check disk space availability +- Verify write access to output directory +- Try different output directory with `--output-dir` + +## Architecture + +### Skill Category + +**Infrastructure** – Plugin.build is part of the distribution layer, preparing plugins for deployment. + +### Design Principles + +- **Validation First**: Check all handlers before packaging +- **Complete Packages**: Include all necessary dependencies +- **Reproducible**: Same source creates identical packages +- **Verifiable**: Checksums ensure package integrity +- **Transparent**: Detailed reporting of included files +- **Flexible**: Support multiple archive formats + +### Package Philosophy + +The package includes everything needed to run the plugin: +- **Skills**: All command handlers and manifests +- **Utilities**: Core betty package for shared functionality +- **Registries**: Skill, command, and hook definitions +- **Dependencies**: Python requirements +- **Documentation**: README, LICENSE, CHANGELOG + +This creates a self-contained, portable plugin distribution. + +## See Also + +- **plugin.sync** – Generate plugin.yaml ([SKILL.md](../plugin.sync/SKILL.md)) +- **skill.define** – Validate and register skills ([SKILL.md](../skill.define/SKILL.md)) +- **registry.update** – Update registries ([SKILL.md](../registry.update/SKILL.md)) +- **Betty Distribution** – Plugin marketplace guide ([docs/distribution.md](../../docs/distribution.md)) + +## Dependencies + +- **plugin.sync**: Generate plugin.yaml before building +- **betty.config**: Configuration constants and paths +- **betty.logging_utils**: Logging infrastructure + +## Status + +**Active** – Production-ready infrastructure skill + +## Version History + +- **0.1.0** (Oct 2025) – Initial implementation with tar.gz/zip support, validation, and checksums diff --git a/skills/plugin.build/__init__.py b/skills/plugin.build/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/plugin.build/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/plugin.build/plugin_build.py b/skills/plugin.build/plugin_build.py new file mode 100755 index 0000000..ead764f --- /dev/null +++ b/skills/plugin.build/plugin_build.py @@ -0,0 +1,648 @@ +#!/usr/bin/env python3 +""" +plugin_build.py - Implementation of the plugin.build Skill +Bundles plugin directory into a deployable Claude Code plugin package. +""" + +import os +import sys +import json +import yaml +import tarfile +import zipfile +import hashlib +import shutil +from typing import Dict, Any, List, Optional, Tuple +from datetime import datetime, timezone +from pathlib import Path +from pydantic import ValidationError as PydanticValidationError + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.telemetry_capture import capture_skill_execution +from betty.models import PluginManifest +from utils.telemetry_utils import capture_telemetry + +logger = setup_logger(__name__) + + +def load_plugin_yaml(plugin_path: str) -> Dict[str, Any]: + """ + Load and parse plugin.yaml file. + + Args: + plugin_path: Path to plugin.yaml + + Returns: + Parsed plugin configuration + + Raises: + FileNotFoundError: If plugin.yaml doesn't exist + yaml.YAMLError: If YAML is invalid + ValueError: If schema validation fails + """ + try: + with open(plugin_path) as f: + config = yaml.safe_load(f) + + # Validate with Pydantic schema + try: + PluginManifest.model_validate(config) + logger.info(f"✅ Loaded and validated plugin.yaml from {plugin_path}") + except PydanticValidationError as exc: + errors = [] + for error in exc.errors(): + field = ".".join(str(loc) for loc in error["loc"]) + message = error["msg"] + errors.append(f"{field}: {message}") + error_msg = f"Plugin schema validation failed: {'; '.join(errors)}" + logger.error(f"❌ {error_msg}") + raise ValueError(error_msg) + + return config + except FileNotFoundError: + logger.error(f"❌ plugin.yaml not found: {plugin_path}") + raise + except yaml.YAMLError as e: + logger.error(f"❌ Invalid YAML in {plugin_path}: {e}") + raise + + +def validate_entrypoints(config: Dict[str, Any], base_dir: str) -> Tuple[List[Dict], List[str]]: + """ + Validate that all entrypoint handlers exist on disk. + + Args: + config: Plugin configuration + base_dir: Base directory for the plugin + + Returns: + Tuple of (valid_entrypoints, missing_files) + """ + valid_entrypoints = [] + missing_files = [] + + commands = config.get("commands", []) + logger.info(f"📝 Validating {len(commands)} command entrypoints...") + + for command in commands: + name = command.get("name", "unknown") + handler = command.get("handler", {}) + script_path = handler.get("script", "") + + if not script_path: + missing_files.append(f"Command '{name}': no handler script specified") + continue + + full_path = os.path.join(base_dir, script_path) + + if os.path.exists(full_path): + valid_entrypoints.append({ + "command": name, + "handler": script_path, + "runtime": handler.get("runtime", "python"), + "path": full_path + }) + logger.debug(f" ✅ {name}: {script_path}") + else: + missing_files.append(f"Command '{name}': handler not found at {script_path}") + logger.warning(f" ❌ {name}: {script_path} (not found)") + + return valid_entrypoints, missing_files + + +def gather_package_files(config: Dict[str, Any], base_dir: str) -> List[Tuple[str, str]]: + """ + Gather all files that need to be included in the package. + + Args: + config: Plugin configuration + base_dir: Base directory for the plugin + + Returns: + List of (source_path, archive_path) tuples + """ + files_to_package = [] + + # Always include plugin.yaml + plugin_yaml_path = os.path.join(base_dir, "plugin.yaml") + if os.path.exists(plugin_yaml_path): + files_to_package.append((plugin_yaml_path, "plugin.yaml")) + logger.debug(" + plugin.yaml") + + # Gather all skill directories from commands + skill_dirs = set() + commands = config.get("commands", []) + + for command in commands: + handler = command.get("handler", {}) + script_path = handler.get("script", "") + + if script_path.startswith("skills/"): + # Extract skill directory (e.g., "skills/api.validate" from "skills/api.validate/api_validate.py") + parts = script_path.split("/") + if len(parts) >= 2: + skill_dir = f"{parts[0]}/{parts[1]}" + skill_dirs.add(skill_dir) + + # Add all files from skill directories + logger.info(f"📦 Gathering files from {len(skill_dirs)} skill directories...") + for skill_dir in sorted(skill_dirs): + skill_path = os.path.join(base_dir, skill_dir) + if os.path.isdir(skill_path): + for root, dirs, files in os.walk(skill_path): + # Skip __pycache__ and .pyc files + dirs[:] = [d for d in dirs if d != "__pycache__"] + + for file in files: + if file.endswith(".pyc") or file.startswith("."): + continue + + source_path = os.path.join(root, file) + rel_path = os.path.relpath(source_path, base_dir) + files_to_package.append((source_path, rel_path)) + logger.debug(f" + {rel_path}") + + # Add betty/ utility package + betty_dir = os.path.join(base_dir, "betty") + if os.path.isdir(betty_dir): + logger.info("📦 Adding betty/ utility package...") + for root, dirs, files in os.walk(betty_dir): + dirs[:] = [d for d in dirs if d != "__pycache__"] + + for file in files: + if file.endswith(".pyc") or file.startswith("."): + continue + + source_path = os.path.join(root, file) + rel_path = os.path.relpath(source_path, base_dir) + files_to_package.append((source_path, rel_path)) + logger.debug(f" + {rel_path}") + + # Add registry/ files + registry_dir = os.path.join(base_dir, "registry") + if os.path.isdir(registry_dir): + logger.info("📦 Adding registry/ files...") + for file in os.listdir(registry_dir): + if file.endswith(".json"): + source_path = os.path.join(registry_dir, file) + rel_path = f"registry/{file}" + files_to_package.append((source_path, rel_path)) + logger.debug(f" + {rel_path}") + + # Add optional files if they exist + optional_files = [ + "requirements.txt", + "README.md", + "LICENSE", + "CHANGELOG.md" + ] + + for filename in optional_files: + file_path = os.path.join(base_dir, filename) + if os.path.exists(file_path): + files_to_package.append((file_path, filename)) + logger.debug(f" + {filename}") + + logger.info(f"📦 Total files to package: {len(files_to_package)}") + return files_to_package + + +def create_tarball(files: List[Tuple[str, str]], output_path: str, plugin_name: str) -> str: + """ + Create a .tar.gz archive. + + Args: + files: List of (source_path, archive_path) tuples + output_path: Output directory + plugin_name: Base name for the archive + + Returns: + Path to created archive + """ + archive_path = os.path.join(output_path, f"{plugin_name}.tar.gz") + + logger.info(f"🗜️ Creating tar.gz archive: {archive_path}") + + with tarfile.open(archive_path, "w:gz") as tar: + for source_path, archive_path_in_tar in files: + # Add files with plugin name prefix + arcname = f"{plugin_name}/{archive_path_in_tar}" + tar.add(source_path, arcname=arcname) + logger.debug(f" Added: {arcname}") + + return archive_path + + +def create_zip(files: List[Tuple[str, str]], output_path: str, plugin_name: str) -> str: + """ + Create a .zip archive. + + Args: + files: List of (source_path, archive_path) tuples + output_path: Output directory + plugin_name: Base name for the archive + + Returns: + Path to created archive + """ + archive_path = os.path.join(output_path, f"{plugin_name}.zip") + + logger.info(f"🗜️ Creating zip archive: {archive_path}") + + with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zf: + for source_path, archive_path_in_zip in files: + # Add files with plugin name prefix + arcname = f"{plugin_name}/{archive_path_in_zip}" + zf.write(source_path, arcname=arcname) + logger.debug(f" Added: {arcname}") + + return archive_path + + +def calculate_checksum(file_path: str) -> Dict[str, str]: + """ + Calculate checksums for the package file. + + Args: + file_path: Path to the package file + + Returns: + Dictionary with md5 and sha256 checksums + """ + logger.info("🔐 Calculating checksums...") + + md5_hash = hashlib.md5() + sha256_hash = hashlib.sha256() + + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + md5_hash.update(chunk) + sha256_hash.update(chunk) + + checksums = { + "md5": md5_hash.hexdigest(), + "sha256": sha256_hash.hexdigest() + } + + logger.info(f" MD5: {checksums['md5']}") + logger.info(f" SHA256: {checksums['sha256']}") + + return checksums + + +def generate_build_report( + config: Dict[str, Any], + valid_entrypoints: List[Dict], + missing_files: List[str], + package_path: str, + checksums: Dict[str, str], + files_count: int +) -> Dict[str, Any]: + """ + Generate build report JSON. + + Args: + config: Plugin configuration + valid_entrypoints: List of validated entrypoints + missing_files: List of missing files + package_path: Path to created package + checksums: Package checksums + files_count: Number of files packaged + + Returns: + Build report dictionary + """ + file_size = os.path.getsize(package_path) + + report = { + "build_timestamp": datetime.now(timezone.utc).isoformat(), + "plugin": { + "name": config.get("name"), + "version": config.get("version"), + "description": config.get("description") + }, + "validation": { + "total_commands": len(config.get("commands", [])), + "valid_entrypoints": len(valid_entrypoints), + "missing_files": missing_files, + "has_errors": len(missing_files) > 0 + }, + "package": { + "path": package_path, + "size_bytes": file_size, + "size_human": f"{file_size / 1024 / 1024:.2f} MB" if file_size > 1024 * 1024 else f"{file_size / 1024:.2f} KB", + "files_count": files_count, + "format": "tar.gz" if package_path.endswith(".tar.gz") else "zip", + "checksums": checksums + }, + "entrypoints": valid_entrypoints + } + + return report + + +def generate_manifest( + config: Dict[str, Any], + valid_entrypoints: List[Dict], + checksums: Dict[str, str], + package_path: str +) -> Dict[str, Any]: + """ + Generate manifest.json for the plugin package. + + Args: + config: Plugin configuration + valid_entrypoints: List of validated entrypoints + checksums: Package checksums + package_path: Path to created package + + Returns: + Manifest dictionary + """ + file_size = os.path.getsize(package_path) + package_filename = os.path.basename(package_path) + + manifest = { + "name": config.get("name"), + "version": config.get("version"), + "description": config.get("description"), + "author": config.get("author", {}), + "license": config.get("license"), + "metadata": { + "homepage": config.get("metadata", {}).get("homepage"), + "repository": config.get("metadata", {}).get("repository"), + "documentation": config.get("metadata", {}).get("documentation"), + "tags": config.get("metadata", {}).get("tags", []), + "generated_at": datetime.now(timezone.utc).isoformat() + }, + "requirements": { + "python": config.get("requirements", {}).get("python"), + "packages": config.get("requirements", {}).get("packages", []) + }, + "permissions": config.get("permissions", []), + "package": { + "filename": package_filename, + "size_bytes": file_size, + "checksums": checksums + }, + "entrypoints": [ + { + "command": ep["command"], + "handler": ep["handler"], + "runtime": ep["runtime"] + } + for ep in valid_entrypoints + ], + "commands_count": len(valid_entrypoints), + "agents": [ + { + "name": agent.get("name"), + "description": agent.get("description") + } + for agent in config.get("agents", []) + ] + } + + return manifest + + +def create_plugin_preview(config: Dict[str, Any], output_path: str) -> Optional[str]: + """ + Create plugin.preview.yaml with current plugin configuration. + This allows reviewing changes before overwriting plugin.yaml. + + Args: + config: Plugin configuration + output_path: Directory to write preview file + + Returns: + Path to preview file or None if creation failed + """ + try: + preview_path = os.path.join(output_path, "plugin.preview.yaml") + + # Add preview metadata + preview_config = config.copy() + if "metadata" not in preview_config: + preview_config["metadata"] = {} + + preview_config["metadata"]["preview_generated_at"] = datetime.now(timezone.utc).isoformat() + preview_config["metadata"]["preview_note"] = "Review before applying to plugin.yaml" + + with open(preview_path, "w") as f: + yaml.dump(preview_config, f, default_flow_style=False, sort_keys=False) + + logger.info(f"📋 Preview file created: {preview_path}") + return preview_path + + except Exception as e: + logger.warning(f"⚠️ Failed to create preview file: {e}") + return None + + +def build_plugin( + plugin_path: str = None, + output_format: str = "tar.gz", + output_dir: str = None +) -> Dict[str, Any]: + """ + Main build function. + + Args: + plugin_path: Path to plugin.yaml (defaults to ./plugin.yaml) + output_format: Package format (tar.gz or zip) + output_dir: Output directory (defaults to ./dist) + + Returns: + Build result dictionary + """ + # Track execution time for telemetry + start_time = datetime.now(timezone.utc) + + # Set defaults + if plugin_path is None: + plugin_path = os.path.join(BASE_DIR, "plugin.yaml") + + if output_dir is None: + output_dir = os.path.join(BASE_DIR, "dist") + + # Normalize format + output_format = output_format.lower() + if output_format not in ["tar.gz", "zip"]: + raise ValueError(f"Unsupported output format: {output_format}. Use 'tar.gz' or 'zip'") + + logger.info("🏗️ Starting plugin build...") + logger.info(f"📄 Plugin: {plugin_path}") + logger.info(f"📦 Format: {output_format}") + logger.info(f"📁 Output: {output_dir}") + + # Load plugin.yaml + config = load_plugin_yaml(plugin_path) + + # Get base directory (parent of plugin.yaml) + base_dir = os.path.dirname(os.path.abspath(plugin_path)) + + # Validate entrypoints + valid_entrypoints, missing_files = validate_entrypoints(config, base_dir) + + if missing_files: + logger.warning(f"⚠️ Found {len(missing_files)} missing files:") + for missing in missing_files: + logger.warning(f" - {missing}") + + # Gather files to package + files_to_package = gather_package_files(config, base_dir) + + # Create output directory + os.makedirs(output_dir, exist_ok=True) + + # Generate package name + plugin_name = config.get("name", "plugin") + plugin_version = config.get("version", "1.0.0") + package_basename = f"{plugin_name}-{plugin_version}" + + # Create package + if output_format == "tar.gz": + package_path = create_tarball(files_to_package, output_dir, package_basename) + else: + package_path = create_zip(files_to_package, output_dir, package_basename) + + # Calculate checksums + checksums = calculate_checksum(package_path) + + # Generate build report + build_report = generate_build_report( + config, + valid_entrypoints, + missing_files, + package_path, + checksums, + len(files_to_package) + ) + + # Write build report JSON + report_path = os.path.join(output_dir, f"{package_basename}-build-report.json") + with open(report_path, "w") as f: + json.dump(build_report, f, indent=2) + + logger.info(f"📊 Build report: {report_path}") + + # Generate and write manifest.json + manifest = generate_manifest(config, valid_entrypoints, checksums, package_path) + manifest_path = os.path.join(output_dir, "manifest.json") + with open(manifest_path, "w") as f: + json.dump(manifest, f, indent=2) + + logger.info(f"📋 Manifest: {manifest_path}") + + # Create plugin preview (optional) + preview_path = create_plugin_preview(config, output_dir) + + # Build result + result = { + "ok": not build_report["validation"]["has_errors"], + "status": "success" if not missing_files else "success_with_warnings", + "package_path": package_path, + "report_path": report_path, + "manifest_path": manifest_path, + "preview_path": preview_path, + "build_report": build_report, + "manifest": manifest + } + + # Calculate execution duration and capture telemetry + end_time = datetime.now(timezone.utc) + duration_ms = int((end_time - start_time).total_seconds() * 1000) + + capture_skill_execution( + skill_name="plugin.build", + inputs={ + "plugin_path": plugin_path, + "output_format": output_format, + "output_dir": output_dir + }, + status="success" if result["ok"] else "success_with_warnings", + duration_ms=duration_ms, + caller="cli", + plugin_name=config.get("name"), + plugin_version=config.get("version"), + files_count=len(files_to_package), + package_size_bytes=os.path.getsize(package_path), + warnings_count=len(missing_files) + ) + + return result + + +@capture_telemetry(skill_name="plugin.build", caller="cli") +def main(): + """Main CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Build deployable Claude Code plugin package" + ) + parser.add_argument( + "plugin_path", + nargs="?", + default=None, + help="Path to plugin.yaml (defaults to ./plugin.yaml)" + ) + parser.add_argument( + "--format", + choices=["tar.gz", "zip"], + default="tar.gz", + help="Package format (default: tar.gz)" + ) + parser.add_argument( + "--output-dir", + default=None, + help="Output directory (default: ./dist)" + ) + + args = parser.parse_args() + + try: + result = build_plugin( + plugin_path=args.plugin_path, + output_format=args.format, + output_dir=args.output_dir + ) + + # Print summary + report = result["build_report"] + logger.info("\n" + "=" * 60) + logger.info("🎉 BUILD COMPLETE") + logger.info("=" * 60) + logger.info(f"📦 Package: {result['package_path']}") + logger.info(f"📊 Report: {result['report_path']}") + logger.info(f"📋 Manifest: {result['manifest_path']}") + if result.get('preview_path'): + logger.info(f"👁️ Preview: {result['preview_path']}") + logger.info(f"✅ Commands: {report['validation']['valid_entrypoints']}/{report['validation']['total_commands']}") + logger.info(f"📏 Size: {report['package']['size_human']}") + logger.info(f"📝 Files: {report['package']['files_count']}") + + if report["validation"]["missing_files"]: + logger.warning(f"⚠️ Warnings: {len(report['validation']['missing_files'])}") + + logger.info("=" * 60) + + print(json.dumps(result, indent=2)) + sys.exit(0 if result["ok"] else 1) + + except Exception as e: + logger.error(f"❌ Build failed: {e}") + result = { + "ok": False, + "status": "failed", + "error": str(e) + } + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/plugin.build/skill.yaml b/skills/plugin.build/skill.yaml new file mode 100644 index 0000000..f3175cb --- /dev/null +++ b/skills/plugin.build/skill.yaml @@ -0,0 +1,58 @@ +name: plugin.build +version: 0.1.0 +description: > + Automatically bundle a plugin directory (or the whole repo) into a deployable Claude Code plugin package. + Gathers all declared entrypoints, validates handler files exist, and packages everything into .tar.gz or .zip under /dist. +inputs: + - name: plugin_path + type: string + required: false + description: Path to plugin.yaml (defaults to ./plugin.yaml) + - name: output_format + type: string + required: false + description: Package format (tar.gz or zip, defaults to tar.gz) + - name: output_dir + type: string + required: false + description: Output directory (defaults to ./dist) +outputs: + - name: plugin_package + type: file + description: Packaged plugin archive (.tar.gz or .zip) + - name: build_report + type: object + description: JSON report with validated entrypoints, missing files, and package checksum +dependencies: + - plugin.sync +status: active + +entrypoints: + - command: /plugin/build + handler: plugin_build.py + runtime: python + description: > + Bundle plugin directory into deployable package. Validates all entrypoints and handlers before packaging. + parameters: + - name: plugin_path + type: string + required: false + description: Path to plugin.yaml (defaults to ./plugin.yaml) + - name: output_format + type: string + required: false + description: Package format (tar.gz or zip, defaults to tar.gz) + - name: output_dir + type: string + required: false + description: Output directory (defaults to ./dist) + permissions: + - filesystem:read + - filesystem:write + +tags: + - plugin + - packaging + - build + - deployment + - distribution diff --git a/skills/plugin.publish/SKILL.md b/skills/plugin.publish/SKILL.md new file mode 100644 index 0000000..3594c5c --- /dev/null +++ b/skills/plugin.publish/SKILL.md @@ -0,0 +1,1023 @@ +--- +name: Plugin Publish +description: Publish bundled plugin packages to local, marketplace, or GitHub Releases with dry-run and validation modes +--- + +# plugin.publish + +## Overview + +**plugin.publish** is the publication tool that distributes your bundled Betty Framework plugin packages to various targets. It validates package integrity via SHA256 checksums and handles publication to local directories, Claude Marketplace endpoints, or GitHub Releases with automatic creation support. Includes `--dry-run` and `--validate-only` modes for safe testing. + +## Purpose + +Automates secure plugin distribution by: +- **Validating** SHA256 checksums to ensure package integrity +- **Publishing** to local directories for testing and archival +- **Uploading** to Claude Marketplace API endpoint (simulated, ready for production) +- **Creating** GitHub Releases automatically using gh CLI +- **Tracking** publication metadata for auditing and governance +- **Testing** with dry-run mode before actual publication +- **Validating** packages without publishing using validate-only mode + +This ensures consistent, traceable, and secure plugin distribution across all deployment targets. + +## What It Does + +1. **Validates Package**: Verifies the .tar.gz file exists and is readable +2. **Calculates Checksums**: Computes MD5 and SHA256 hashes for integrity verification +3. **Validates SHA256**: Compares against expected checksum from manifest.json +4. **Loads Metadata**: Extracts plugin info from manifest.json (name, version, author, etc.) +5. **Publishes to Target**: + - **local**: Copies to `dist/published/` with metadata + - **marketplace**: POSTs JSON metadata to Claude Marketplace API (simulated) + - **gh-release**: Creates GitHub Release using gh CLI (with auto-create option) +6. **Supports Modes**: + - **--dry-run**: Shows what would be done without making changes + - **--validate-only**: Only validates checksums without publishing + - **--auto-create**: Automatically creates GitHub Release using gh CLI +7. **Generates Metadata**: Creates publication records for auditing +8. **Reports Results**: Returns publication status with paths and checksums + +## Usage + +### Basic Usage (Local Target) + +```bash +python skills/plugin.publish/plugin_publish.py dist/betty-framework-1.0.0.tar.gz +``` + +Publishes with defaults: +- Target: `local` (dist/published/) +- Checksum validation: Auto-detected from manifest.json + +### Via Betty CLI + +```bash +/plugin/publish dist/betty-framework-1.0.0.tar.gz +``` + +### Publish to Specific Target + +```bash +# Publish to local directory +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=local + +# Publish to Claude Marketplace (simulated) +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace + +# Prepare GitHub Release (files only, manual creation needed) +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=gh-release + +# Create GitHub Release automatically using gh CLI +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=gh-release \ + --auto-create +``` + +### Dry Run and Validation Modes + +```bash +# Dry run - show what would happen without making changes +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace \ + --dry-run + +# Validate only - check checksums without publishing +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --validate-only + +# Dry run with GitHub Release +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=gh-release \ + --auto-create \ + --dry-run +``` + +### With Explicit Checksum Validation + +```bash +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=local \ + --sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +``` + +### With Custom Manifest Path + +```bash +python skills/plugin.publish/plugin_publish.py \ + /tmp/betty-framework-1.0.0.tar.gz \ + --target=release \ + --manifest=/tmp/manifest.json +``` + +### With Custom Marketplace Endpoint + +```bash +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace \ + --endpoint=https://api.example.com/plugins/upload +``` + +## Command-Line Arguments + +| Argument | Type | Default | Description | +|----------|------|---------|-------------| +| `package_path` | Positional | Required | Path to the .tar.gz package file | +| `--target` | Option | `local` | Publication target: `local`, `marketplace`, or `gh-release` | +| `--sha256` | Option | Auto-detect | Expected SHA256 checksum for validation | +| `--manifest` | Option | Auto-detect | Path to manifest.json | +| `--endpoint` | Option | Claude Marketplace | Marketplace API endpoint URL (for `marketplace` target) | +| `--dry-run` | Flag | False | Show what would be done without making changes | +| `--validate-only` | Flag | False | Only validate checksums without publishing | +| `--auto-create` | Flag | False | Automatically create GitHub Release using gh CLI (for `gh-release` target) | + +## Publication Targets + +### Local Target + +**Purpose**: Copy package to local published directory for testing, archival, or internal distribution. + +**Output Location**: `dist/published/` + +**Generated Files**: +- `{package-name}.tar.gz` - Copied package file +- `{package-name}.tar.gz.publish.json` - Publication metadata + +**Use Cases**: +- Local testing before remote publication +- Internal company archives +- Offline distribution +- Backup copies + +**Example**: +```bash +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=local +``` + +**Output**: +``` +dist/published/ +├── betty-framework-1.0.0.tar.gz +└── betty-framework-1.0.0.tar.gz.publish.json +``` + +### Marketplace Target + +**Purpose**: Upload plugin metadata to Claude Marketplace API endpoint (currently simulated). + +**Output Location**: `dist/published/marketplace/` + +**Generated Files**: +- `{package-name}.tar.gz.marketplace-publish.json` - Simulation log with request/response + +**Use Cases**: +- Publish to Claude Code Marketplace +- Upload to custom plugin repository +- Submit to enterprise plugin registry + +**Current Implementation**: SIMULATED +- Generates complete HTTP POST request with JSON metadata +- Returns mock successful response +- No actual network request made +- Ready for real implementation (add requests library) + +**Example**: +```bash +# Standard marketplace publication (simulated) +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace + +# With custom endpoint +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace \ + --endpoint=https://marketplace.claude.ai/api/v1/plugins + +# Dry run first to see what would happen +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace \ + --dry-run +``` + +**JSON Metadata Structure**: +```json +{ + "plugin": { + "name": "betty-framework", + "version": "1.0.0", + "description": "...", + "author": { ... }, + "license": "MIT", + "homepage": "...", + "repository": "...", + "tags": [...], + "betty_version": ">=0.1.0" + }, + "package": { + "filename": "betty-framework-1.0.0.tar.gz", + "size_bytes": 524288, + "checksums": { + "md5": "...", + "sha256": "..." + } + }, + "submitted_at": "2025-10-24T12:00:00.000000+00:00" +} +``` + +### GitHub Release Target + +**Purpose**: Create GitHub Releases with auto-generated release notes and automatic upload support. + +**Output Location**: `dist/published/releases/` + +**Generated Files**: +- `{package-name}.tar.gz` - Copied package file +- `RELEASE_NOTES_v{version}.md` - Auto-generated release notes +- `{package-name}.tar.gz.release.json` - Release metadata + +**Use Cases**: +- GitHub Releases publication (manual or automatic) +- Public open-source distribution +- Versioned releases with auto-generated notes +- Official product releases + +**Modes**: +1. **Preparation Only** (default): Prepares files for manual release creation +2. **Automatic Creation** (`--auto-create`): Uses gh CLI to create release automatically + +**Examples**: +```bash +# Prepare release files only (manual upload needed) +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=gh-release + +# Automatically create GitHub Release using gh CLI +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=gh-release \ + --auto-create + +# Dry run to see what would be created +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=gh-release \ + --auto-create \ + --dry-run +``` + +**Requirements for Auto-Create**: +- GitHub CLI (`gh`) must be installed +- Must be authenticated: `gh auth login` +- Must have write access to repository +- Tag must not already exist + +**Output**: +``` +dist/published/releases/ +├── betty-framework-1.0.0.tar.gz +├── RELEASE_NOTES_v1.0.0.md +└── betty-framework-1.0.0.tar.gz.release.json +``` + +## Operating Modes + +### Dry Run Mode (`--dry-run`) + +**Purpose**: Test publication without making any changes + +**Behavior**: +- Validates package and checksums +- Shows all operations that would be performed +- Does NOT create files, directories, or network requests +- Does NOT create GitHub Releases +- Returns success if all validation passes + +**Use Cases**: +- Test before actual publication +- Verify configuration is correct +- Preview what will happen +- CI/CD validation steps + +**Example**: +```bash +# Test local publication +python skills/plugin.publish/plugin_publish.py \ + dist/betty-1.0.0.tar.gz \ + --target=local \ + --dry-run + +# Test marketplace publication +python skills/plugin.publish/plugin_publish.py \ + dist/betty-1.0.0.tar.gz \ + --target=marketplace \ + --dry-run + +# Test GitHub Release creation +python skills/plugin.publish/plugin_publish.py \ + dist/betty-1.0.0.tar.gz \ + --target=gh-release \ + --auto-create \ + --dry-run +``` + +**Output Example**: +``` +🔍 DRY RUN MODE - No changes will be made +📦 Publishing to local directory... + Would create directory: /home/user/betty/dist/published + Would copy: /home/user/betty/dist/betty-1.0.0.tar.gz + To: /home/user/betty/dist/published/betty-1.0.0.tar.gz + Would write metadata to: /home/user/betty/dist/published/betty-1.0.0.tar.gz.publish.json +✅ Dry run completed successfully +``` + +### Validate Only Mode (`--validate-only`) + +**Purpose**: Validate package integrity without publishing + +**Behavior**: +- Validates package file exists +- Calculates MD5 and SHA256 checksums +- Compares against expected checksums from manifest.json +- Exits immediately after validation +- Does NOT publish to any target + +**Use Cases**: +- Verify package integrity before distribution +- CI/CD checksum validation +- Security audits +- Package verification after download + +**Example**: +```bash +# Validate package checksums +python skills/plugin.publish/plugin_publish.py \ + dist/betty-1.0.0.tar.gz \ + --validate-only +``` + +**Output Example**: +``` +🔍 VALIDATE ONLY MODE ENABLED + +📦 Package: dist/betty-1.0.0.tar.gz +🎯 Target: local +📄 Manifest: dist/manifest.json + +🔍 Validating package checksums... +🔐 Calculating checksums... + MD5: d41d8cd98f00b204e9800998ecf8427e + SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +✅ SHA256 checksum validation: PASSED + +================================================================================ +✅ VALIDATION SUCCESSFUL +================================================================================ + +Package is valid and ready for publication. +To publish, run without --validate-only flag: + python skills/plugin.publish/plugin_publish.py dist/betty-1.0.0.tar.gz --target=local +``` + +**Note**: `--validate-only` takes precedence over `--dry-run` if both are specified. + +## Output Files + +### Publication Metadata (Local Target) + +**File**: `{package-name}.tar.gz.publish.json` + +**Structure**: +```json +{ + "published_at": "2025-10-24T12:00:00.000000+00:00", + "target": "local", + "package": { + "filename": "betty-framework-1.0.0.tar.gz", + "path": "/home/user/betty/dist/published/betty-framework-1.0.0.tar.gz", + "size_bytes": 524288, + "checksums": { + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + "metadata": { + "name": "betty-framework", + "version": "1.0.0", + "description": "Betty Framework is RiskExec's system...", + "author": { + "name": "RiskExec", + "email": "platform@riskexec.com" + }, + "license": "MIT" + } +} +``` + +### Marketplace Publication Simulation Log + +**File**: `{package-name}.tar.gz.marketplace-publish.json` + +**Structure**: +```json +{ + "simulated_at": "2025-10-24T12:00:00.000000+00:00", + "target": "marketplace", + "endpoint": "https://marketplace.claude.ai/api/v1/plugins", + "request": { + "method": "POST", + "url": "https://marketplace.claude.ai/api/v1/plugins", + "headers": { + "Content-Type": "application/json", + "X-Plugin-Name": "betty-framework", + "X-Plugin-Version": "1.0.0", + "X-Package-SHA256": "..." + }, + "json": { + "plugin": { ... }, + "package": { ... }, + "submitted_at": "..." + } + }, + "response": { + "status": 200, + "body": { + "success": true, + "message": "Plugin published successfully", + "plugin": { + "id": "betty-framework-1.0.0", + "name": "betty-framework", + "version": "1.0.0", + "published_at": "2025-10-24T12:00:00.000000+00:00", + "download_url": "...", + "listing_url": "https://marketplace.claude.ai/plugins/betty-framework" + }, + "checksums": { ... } + } + }, + "dry_run": false, + "note": "This is a simulated request. No actual HTTP request was made. To enable real publication, add requests library implementation." +} +``` + +### GitHub Release Notes + +**File**: `RELEASE_NOTES_v{version}.md` + +**Auto-generated Content**: +```markdown +# betty-framework v1.0.0 + +## Release Information + +- **Version:** 1.0.0 +- **Released:** 2025-10-24 +- **Package:** `betty-framework-1.0.0.tar.gz` + +## Checksums + +Verify the integrity of your download: + +``` +MD5: d41d8cd98f00b204e9800998ecf8427e +SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +``` + +## Installation + +1. Download the package: `betty-framework-1.0.0.tar.gz` +2. Verify checksums (see above) +3. Extract: `tar -xzf betty-framework-1.0.0.tar.gz` +4. Install dependencies: `pip install -r requirements.txt` +5. Run: Follow instructions in README.md + +## Description + +Betty Framework is RiskExec's system for structured, auditable AI-assisted engineering... + +## GitHub CLI Commands + +To create this release using GitHub CLI: + +```bash +# Create release +gh release create v1.0.0 \ + --title "betty-framework v1.0.0" \ + --notes-file RELEASE_NOTES.md \ + betty-framework-1.0.0.tar.gz +``` + +## Manual Upload + +1. Go to: https://github.com/YOUR_ORG/YOUR_REPO/releases/new +2. Tag version: `v1.0.0` +3. Release title: `betty-framework v1.0.0` +4. Upload: `betty-framework-1.0.0.tar.gz` +5. Add checksums to release notes +6. Publish release +``` + +### Release Metadata + +**File**: `{package-name}.tar.gz.release.json` + +**Structure**: +```json +{ + "prepared_at": "2025-10-24T12:00:00.000000+00:00", + "target": "gh-release", + "version": "1.0.0", + "name": "betty-framework", + "package": { + "filename": "betty-framework-1.0.0.tar.gz", + "path": "/home/user/betty/dist/published/releases/betty-framework-1.0.0.tar.gz", + "size_bytes": 524288, + "checksums": { + "md5": "...", + "sha256": "..." + } + }, + "release_notes_path": "/home/user/betty/dist/published/releases/RELEASE_NOTES_v1.0.0.md", + "github_cli_command": "gh release create v1.0.0 --title \"betty-framework v1.0.0\" --notes-file ...", + "metadata": { + "name": "betty-framework", + "version": "1.0.0", + "description": "...", + "author": { ... }, + "license": "MIT" + }, + "dry_run": false, + "auto_created": true, + "github_release_url": "https://github.com/user/repo/releases/tag/v1.0.0" +} +``` + +**Note**: The `auto_created` field is `true` if `--auto-create` was used and succeeded, and `github_release_url` will contain the actual release URL. + +## Checksum Validation + +### Automatic Validation + +By default, `plugin.publish` automatically detects and validates checksums from `manifest.json`: + +```bash +python skills/plugin.publish/plugin_publish.py dist/betty-framework-1.0.0.tar.gz +``` + +**Process**: +1. Looks for `dist/manifest.json` +2. Extracts `package.checksums.sha256` +3. Calculates actual SHA256 of package file +4. Compares and validates + +### Manual Validation + +Explicitly provide expected SHA256: + +```bash +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +``` + +### Validation Output + +**Success**: +``` +🔍 Validating package checksums... +🔐 Calculating checksums... + MD5: d41d8cd98f00b204e9800998ecf8427e + SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +✅ SHA256 checksum validation: PASSED +``` + +**Failure**: +``` +🔍 Validating package checksums... +🔐 Calculating checksums... + MD5: d41d8cd98f00b204e9800998ecf8427e + SHA256: abc123... +❌ SHA256 checksum validation: FAILED + Expected: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + Actual: abc123... +``` + +## Workflow Integration + +### Complete Build and Publish Pipeline + +```bash +# Step 1: Build plugin +python skills/plugin.build/plugin_build.py + +# Step 2: Validate package integrity +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --validate-only + +# Step 3: Dry run to test publication +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=local \ + --dry-run + +# Step 4: Publish to local for testing +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=local + +# Step 5: Publish to Claude Marketplace (simulated) +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace + +# Step 6: Create GitHub Release automatically +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=gh-release \ + --auto-create +``` + +### Safe Publication Workflow (Recommended) + +```bash +# 1. Validate first +python skills/plugin.publish/plugin_publish.py \ + dist/betty-1.0.0.tar.gz \ + --validate-only + +# 2. Dry run to preview +python skills/plugin.publish/plugin_publish.py \ + dist/betty-1.0.0.tar.gz \ + --target=marketplace \ + --dry-run + +# 3. Actual publication +python skills/plugin.publish/plugin_publish.py \ + dist/betty-1.0.0.tar.gz \ + --target=marketplace +``` + +### Automated CI/CD Integration + +```yaml +# .github/workflows/publish.yml +name: Publish Plugin + +on: + push: + tags: + - 'v*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Build plugin + run: python skills/plugin.build/plugin_build.py + + - name: Validate package + run: | + python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-*.tar.gz \ + --validate-only + + - name: Publish to Claude Marketplace + run: | + python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-*.tar.gz \ + --target=marketplace + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-*.tar.gz \ + --target=gh-release \ + --auto-create +``` + +### CI/CD with Dry Run Testing + +```yaml +# .github/workflows/test-publish.yml +name: Test Publication + +on: + pull_request: + branches: [main] + +jobs: + test-publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build plugin + run: python skills/plugin.build/plugin_build.py + + - name: Validate package + run: | + python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-*.tar.gz \ + --validate-only + + - name: Test local publish (dry run) + run: | + python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-*.tar.gz \ + --target=local \ + --dry-run + + - name: Test marketplace publish (dry run) + run: | + python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-*.tar.gz \ + --target=marketplace \ + --dry-run + + - name: Test GitHub Release (dry run) + run: | + python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-*.tar.gz \ + --target=gh-release \ + --auto-create \ + --dry-run +``` + +## Error Handling + +### Package Not Found + +``` +❌ Package file not found: /path/to/missing.tar.gz +``` + +**Solution**: Verify package path and run `plugin.build` first. + +### Invalid Target + +``` +❌ Invalid target: prod. Must be one of: local, marketplace, gh-release +``` + +**Solution**: Use one of the valid target values: `local`, `marketplace`, or `gh-release`. + +### Checksum Validation Failed + +``` +❌ SHA256 checksum validation: FAILED +``` + +**Solution**: Package may be corrupted. Rebuild using `plugin.build`. + +### Manifest Not Found + +``` +⚠️ Manifest not found: /path/to/manifest.json +⚠️ No expected checksum provided - skipping validation +``` + +**Solution**: Provide `--manifest` path or place manifest.json in package directory. + +## Advanced Usage + +### Publishing to Multiple Targets + +```bash +#!/bin/bash +PACKAGE="dist/betty-framework-1.0.0.tar.gz" + +# Validate first +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --validate-only + +# Publish to all targets +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=local +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=marketplace +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=gh-release --auto-create + +echo "Published to all targets successfully!" +``` + +### Custom Endpoint Configuration + +```bash +# Development marketplace endpoint +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace \ + --endpoint=https://dev.marketplace.claude.ai/api/v1/plugins + +# Production marketplace endpoint +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --target=marketplace \ + --endpoint=https://marketplace.claude.ai/api/v1/plugins +``` + +### Safe Publication with Validation + +```bash +#!/bin/bash +PACKAGE="dist/betty-framework-1.0.0.tar.gz" + +# Step 1: Validate checksums +echo "Validating package..." +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --validate-only || exit 1 + +# Step 2: Dry run for all targets +echo "Running dry runs..." +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=local --dry-run || exit 1 +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=marketplace --dry-run || exit 1 +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=gh-release --auto-create --dry-run || exit 1 + +# Step 3: Actual publication +echo "Publishing..." +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=local +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=marketplace +python skills/plugin.publish/plugin_publish.py "$PACKAGE" --target=gh-release --auto-create + +echo "All publications completed successfully!" +``` + +### Verification After Publication + +```bash +# Local verification +cd dist/published +sha256sum -c <<< "e3b0c44... betty-framework-1.0.0.tar.gz" + +# Extract and inspect +tar -tzf betty-framework-1.0.0.tar.gz | head -20 + +# Validate manifest +cat betty-framework-1.0.0.tar.gz.publish.json | jq . +``` + +## Dependencies + +- **Python**: 3.11+ +- **Standard Library**: os, sys, json, yaml, hashlib, shutil, pathlib, datetime +- **Betty Modules**: betty.config +- **Future**: requests (for actual remote publication) + +## Related Skills + +- **plugin.build**: Build plugin packages before publishing +- **plugin.sync**: Synchronize plugin.yaml from skill registry +- **registry.update**: Update skill registry entries +- **audit.log**: Log publication events for governance + +## Security Considerations + +### Checksum Validation + +Always validate SHA256 checksums before publication: +- Detects package corruption +- Prevents tampering +- Ensures integrity across distribution channels + +### Publication Tracking + +All publications are logged with: +- Timestamp (UTC) +- Target destination +- Checksums +- Package metadata + +This creates an audit trail for governance and compliance. + +### Remote Publication (When Implemented) + +For actual remote publication: +- Use HTTPS endpoints only +- Authenticate with API tokens (not in source code) +- Verify TLS certificates +- Implement retry logic with exponential backoff +- Log all API responses + +## Troubleshooting + +### Issue: Checksum mismatch after build + +**Symptom**: SHA256 validation fails immediately after building + +**Causes**: +- Manifest.json not generated by plugin.build +- Package file modified after build +- Incorrect manifest path + +**Solution**: +```bash +# Rebuild package +python skills/plugin.build/plugin_build.py + +# Verify manifest exists +ls -la dist/manifest.json + +# Publish with explicit manifest +python skills/plugin.publish/plugin_publish.py \ + dist/betty-framework-1.0.0.tar.gz \ + --manifest=dist/manifest.json +``` + +### Issue: Marketplace publication not actually uploading + +**Symptom**: No network error but file not on marketplace + +**Cause**: Marketplace publication is currently SIMULATED + +**Solution**: This is expected behavior. The simulation creates a complete request structure in: +``` +dist/published/marketplace/{package}.marketplace-publish.json +``` + +For actual HTTP upload, add requests library implementation in `publish_marketplace()`. + +### Issue: GitHub Release fails + +**Symptom**: `gh release create` command fails + +**Causes**: +- GitHub CLI not installed +- Not authenticated with GitHub +- Tag already exists +- No write access to repository + +**Solutions**: +```bash +# Install GitHub CLI +# macOS: brew install gh +# Linux: sudo apt install gh + +# Authenticate +gh auth login + +# Check existing releases +gh release list + +# Delete existing tag if needed +gh release delete v1.0.0 +git push --delete origin v1.0.0 +``` + +## Best Practices + +1. **Use validate-only first**: Always run `--validate-only` before publishing to verify package integrity +2. **Dry run before publishing**: Use `--dry-run` to test publication workflows without making changes +3. **Test locally first**: Publish to `--target=local` before marketplace or GitHub Release +4. **Review release notes**: Check auto-generated notes in `dist/published/releases/RELEASE_NOTES_*.md` +5. **Keep publication metadata**: Retain `.json` files for audit trails and compliance +6. **Use version tags consistently**: Follow semantic versioning (e.g., v1.0.0 format) +7. **Automate via CI/CD**: Use GitHub Actions for consistent, reproducible releases +8. **Verify gh CLI setup**: Test `gh auth status` before using `--auto-create` +9. **Backup published packages**: Keep copies for disaster recovery +10. **Use safe publication workflow**: Validate → Dry run → Local → Remote → Release + +## Future Enhancements + +- [ ] Actual HTTP POST implementation for marketplace target (add requests library) +- [ ] Support for multiple marketplace endpoints (dev, staging, prod) +- [ ] Digital signature support for package verification (GPG signing) +- [ ] Publication rollback mechanism for failed releases +- [ ] Automatic changelog generation from git commits +- [ ] Publication analytics and download tracking +- [ ] Multi-target parallel publication (async) +- [ ] Package verification after publication (download and verify checksums) +- [ ] Support for pre-release and draft GitHub Releases +- [ ] Integration with artifact registries (JFrog, Nexus) +- [ ] Webhook notifications on successful publication +- [ ] Support for package mirroring across multiple registries + +--- + +**Generated by**: Betty Framework +**Version**: 0.1.0 +**Last Updated**: 2025-10-24 diff --git a/skills/plugin.publish/__init__.py b/skills/plugin.publish/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/plugin.publish/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/plugin.publish/plugin_publish.py b/skills/plugin.publish/plugin_publish.py new file mode 100755 index 0000000..d317cd8 --- /dev/null +++ b/skills/plugin.publish/plugin_publish.py @@ -0,0 +1,832 @@ +#!/usr/bin/env python3 +""" +Betty Framework - Plugin Publish Skill +Upload bundled plugin (.tar.gz) to local, gh-release, or marketplace targets. +Supports --dry-run and --validate-only modes. +""" + +import os +import sys +import json +import yaml +import hashlib +import shutil +import logging +import subprocess +from pathlib import Path +from datetime import datetime, timezone +from typing import Dict, Any, Optional, Tuple + +# Add betty module to path +BETTY_HOME = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) + +from betty.config import BASE_DIR, REGISTRY_DIR +from utils.telemetry_utils import capture_telemetry + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(message)s", + handlers=[logging.StreamHandler(sys.stdout)] +) +logger = logging.getLogger(__name__) + +# Global flags +DRY_RUN = False +VALIDATE_ONLY = False + + +def calculate_file_checksum(file_path: str) -> Dict[str, str]: + """ + Calculate checksums for a file. + + Args: + file_path: Path to the file + + Returns: + Dictionary with md5 and sha256 checksums + """ + logger.info("🔐 Calculating checksums...") + + md5_hash = hashlib.md5() + sha256_hash = hashlib.sha256() + + # Stream reading to handle large files efficiently + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): # 4KB chunks + md5_hash.update(chunk) + sha256_hash.update(chunk) + + checksums = { + "md5": md5_hash.hexdigest(), + "sha256": sha256_hash.hexdigest() + } + + logger.info(f" MD5: {checksums['md5']}") + logger.info(f" SHA256: {checksums['sha256']}") + + return checksums + + +def validate_checksum( + package_path: str, + expected_sha256: Optional[str] = None, + manifest_path: Optional[str] = None +) -> Tuple[bool, Dict[str, str]]: + """ + Validate the SHA256 checksum of a package file. + + Args: + package_path: Path to the package file + expected_sha256: Expected SHA256 hash (optional) + manifest_path: Path to manifest.json containing checksums (optional) + + Returns: + Tuple of (is_valid, checksums_dict) + """ + logger.info("🔍 Validating package checksums...") + + # Calculate actual checksums + actual_checksums = calculate_file_checksum(package_path) + + # Try to get expected checksum from manifest if not provided + if not expected_sha256 and manifest_path: + try: + with open(manifest_path, "r", encoding="utf-8") as f: + manifest = json.load(f) + expected_sha256 = manifest.get("package", {}).get("checksums", {}).get("sha256") + except (FileNotFoundError, json.JSONDecodeError, KeyError): + logger.warning(f"⚠️ Could not read checksums from manifest: {manifest_path}") + + # Validate if expected checksum is available + if expected_sha256: + is_valid = actual_checksums["sha256"] == expected_sha256 + if is_valid: + logger.info("✅ SHA256 checksum validation: PASSED") + else: + logger.error("❌ SHA256 checksum validation: FAILED") + logger.error(f" Expected: {expected_sha256}") + logger.error(f" Actual: {actual_checksums['sha256']}") + return is_valid, actual_checksums + else: + logger.warning("⚠️ No expected checksum provided - skipping validation") + return True, actual_checksums + + +def publish_local( + package_path: str, + checksums: Dict[str, str], + metadata: Dict[str, Any], + dry_run: bool = False +) -> Dict[str, Any]: + """ + Publish plugin to local dist/published/ directory. + + Args: + package_path: Path to the package file + checksums: Checksums dictionary + metadata: Additional metadata + dry_run: If True, only show what would be done + + Returns: + Result dictionary with publication details + """ + logger.info("📦 Publishing to local directory...") + + if dry_run: + logger.info("🔍 DRY RUN MODE - No changes will be made") + + # Create published directory + published_dir = os.path.join(BASE_DIR, "dist", "published") + + if not dry_run: + os.makedirs(published_dir, exist_ok=True) + else: + logger.info(f" Would create directory: {published_dir}") + + # Copy package to published directory + package_filename = os.path.basename(package_path) + dest_path = os.path.join(published_dir, package_filename) + + logger.info(f" {'Would copy' if dry_run else 'Copying'}: {package_path}") + logger.info(f" To: {dest_path}") + + if not dry_run: + shutil.copy2(package_path, dest_path) + + # Create publication metadata file + pub_metadata = { + "published_at": datetime.now(timezone.utc).isoformat(), + "target": "local", + "package": { + "filename": package_filename, + "path": dest_path, + "size_bytes": os.path.getsize(package_path), + "checksums": checksums + }, + "metadata": metadata, + "dry_run": dry_run + } + + metadata_path = os.path.join( + published_dir, + f"{package_filename}.publish.json" + ) + + if not dry_run: + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(pub_metadata, f, indent=2) + else: + logger.info(f" Would write metadata to: {metadata_path}") + + if dry_run: + logger.info("✅ Dry run completed successfully") + else: + logger.info("✅ Successfully published to local directory") + logger.info(f" Package: {dest_path}") + logger.info(f" Metadata: {metadata_path}") + + return { + "ok": True, + "target": "local", + "package_path": dest_path, + "metadata_path": metadata_path, + "publication_metadata": pub_metadata, + "dry_run": dry_run + } + + +def publish_marketplace( + package_path: str, + checksums: Dict[str, str], + metadata: Dict[str, Any], + endpoint: str = "https://marketplace.claude.ai/api/v1/plugins", + dry_run: bool = False +) -> Dict[str, Any]: + """ + Publish plugin to Claude Marketplace endpoint. + Currently simulated - actual implementation would use requests library. + + Args: + package_path: Path to the package file + checksums: Checksums dictionary + metadata: Additional metadata + endpoint: API endpoint URL + dry_run: If True, only show what would be done + + Returns: + Result dictionary with publication details + """ + logger.info("🌐 Publishing to Claude Marketplace...") + logger.info(f" Endpoint: {endpoint}") + + if dry_run: + logger.info("🔍 DRY RUN MODE - No actual requests will be made") + + # Prepare API request + package_filename = os.path.basename(package_path) + package_size = os.path.getsize(package_path) + + # Prepare JSON metadata payload for marketplace + marketplace_metadata = { + "plugin": { + "name": metadata.get("name", "unknown"), + "version": metadata.get("version", "unknown"), + "description": metadata.get("description", ""), + "author": metadata.get("author", {}), + "license": metadata.get("license", ""), + "homepage": metadata.get("homepage", ""), + "repository": metadata.get("repository", ""), + "tags": metadata.get("tags", []), + "betty_version": metadata.get("betty_version", ">=0.1.0") + }, + "package": { + "filename": package_filename, + "size_bytes": package_size, + "checksums": { + "md5": checksums["md5"], + "sha256": checksums["sha256"] + } + }, + "submitted_at": datetime.now(timezone.utc).isoformat() + } + + request_payload = { + "method": "POST", + "url": endpoint, + "headers": { + "Content-Type": "application/json", + "X-Plugin-Name": metadata.get("name", "unknown"), + "X-Plugin-Version": metadata.get("version", "unknown"), + "X-Package-SHA256": checksums["sha256"] + }, + "json": marketplace_metadata + } + + # Simulate successful response + simulated_response = { + "status": 200, + "body": { + "success": True, + "message": "Plugin published successfully", + "plugin": { + "id": f"{metadata.get('name')}-{metadata.get('version')}", + "name": metadata.get("name"), + "version": metadata.get("version"), + "published_at": datetime.now(timezone.utc).isoformat(), + "download_url": f"{endpoint.rstrip('/plugins')}/download/{metadata.get('name')}/{metadata.get('version')}/{package_filename}", + "listing_url": f"https://marketplace.claude.ai/plugins/{metadata.get('name')}" + }, + "checksums": checksums + } + } + + # Save simulation log + sim_log_dir = os.path.join(BASE_DIR, "dist", "published", "marketplace") + + if not dry_run: + os.makedirs(sim_log_dir, exist_ok=True) + else: + logger.info(f" Would create directory: {sim_log_dir}") + + sim_log_path = os.path.join( + sim_log_dir, + f"{package_filename}.marketplace-publish.json" + ) + + simulation_log = { + "simulated_at": datetime.now(timezone.utc).isoformat(), + "target": "marketplace", + "endpoint": endpoint, + "request": request_payload, + "response": simulated_response, + "dry_run": dry_run, + "note": "This is a simulated request. No actual HTTP request was made. To enable real publication, add requests library implementation." + } + + if not dry_run: + with open(sim_log_path, "w", encoding="utf-8") as f: + json.dump(simulation_log, f, indent=2) + logger.info("✅ Marketplace publication simulated successfully") + logger.info(f" Simulation log: {sim_log_path}") + else: + logger.info("✅ Dry run completed successfully") + logger.info(f" Would write simulation log to: {sim_log_path}") + + logger.warning("⚠️ NOTE: This is a SIMULATION. No actual HTTP request was made.") + logger.info(f" Would POST JSON metadata to: {endpoint}") + logger.info(f" Package size: {package_size:,} bytes") + + return { + "ok": True, + "target": "marketplace", + "simulated": True, + "endpoint": endpoint, + "simulation_log": sim_log_path if not dry_run else None, + "simulated_response": simulated_response["body"], + "dry_run": dry_run + } + + +def check_gh_cli_available() -> bool: + """ + Check if GitHub CLI (gh) is available. + + Returns: + True if gh CLI is available, False otherwise + """ + try: + result = subprocess.run( + ["gh", "--version"], + capture_output=True, + text=True, + timeout=5 + ) + return result.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def publish_gh_release( + package_path: str, + checksums: Dict[str, str], + metadata: Dict[str, Any], + dry_run: bool = False, + auto_create: bool = False +) -> Dict[str, Any]: + """ + Publish plugin to GitHub Release. + Can either prepare release files or automatically create the release using gh CLI. + + Args: + package_path: Path to the package file + checksums: Checksums dictionary + metadata: Additional metadata + dry_run: If True, only show what would be done + auto_create: If True, automatically create GitHub Release using gh CLI + + Returns: + Result dictionary with release details + """ + logger.info("🚀 Publishing to GitHub Release...") + + if dry_run: + logger.info("🔍 DRY RUN MODE - No changes will be made") + + # Create releases directory + releases_dir = os.path.join(BASE_DIR, "dist", "published", "releases") + + if not dry_run: + os.makedirs(releases_dir, exist_ok=True) + else: + logger.info(f" Would create directory: {releases_dir}") + + package_filename = os.path.basename(package_path) + version = metadata.get("version", "unknown") + name = metadata.get("name", "unknown") + + # Copy package to releases directory + dest_path = os.path.join(releases_dir, package_filename) + + logger.info(f" {'Would copy' if dry_run else 'Copying'}: {package_path}") + logger.info(f" To: {dest_path}") + + if not dry_run: + shutil.copy2(package_path, dest_path) + + # Generate release notes + release_notes = f"""# {name} v{version} + +## Release Information + +- **Version:** {version} +- **Released:** {datetime.now(timezone.utc).strftime('%Y-%m-%d')} +- **Package:** `{package_filename}` + +## Checksums + +Verify the integrity of your download: + +``` +MD5: {checksums['md5']} +SHA256: {checksums['sha256']} +``` + +## Installation + +1. Download the package: `{package_filename}` +2. Verify checksums (see above) +3. Extract: `tar -xzf {package_filename}` +4. Install dependencies: `pip install -r requirements.txt` +5. Run: Follow instructions in README.md + +## Description + +{metadata.get('description', 'No description available.')} + +--- +*Generated by Betty Framework plugin.publish skill* +""" + + release_notes_path = os.path.join(releases_dir, f"RELEASE_NOTES_v{version}.md") + + if not dry_run: + with open(release_notes_path, "w", encoding="utf-8") as f: + f.write(release_notes) + else: + logger.info(f" Would write release notes to: {release_notes_path}") + + # Create release metadata + release_metadata = { + "prepared_at": datetime.now(timezone.utc).isoformat(), + "target": "gh-release", + "version": version, + "name": name, + "package": { + "filename": package_filename, + "path": dest_path, + "size_bytes": os.path.getsize(package_path), + "checksums": checksums + }, + "release_notes_path": release_notes_path, + "github_cli_command": f"gh release create v{version} --title \"{name} v{version}\" --notes-file {release_notes_path} {dest_path}", + "metadata": metadata, + "dry_run": dry_run, + "auto_created": False + } + + metadata_path = os.path.join( + releases_dir, + f"{package_filename}.release.json" + ) + + if not dry_run: + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(release_metadata, f, indent=2) + else: + logger.info(f" Would write metadata to: {metadata_path}") + + # Attempt to create GitHub Release if auto_create is True + gh_release_created = False + gh_release_url = None + + if auto_create and not dry_run: + logger.info("") + logger.info("🤖 Attempting to create GitHub Release automatically...") + + # Check if gh CLI is available + if not check_gh_cli_available(): + logger.error("❌ GitHub CLI (gh) not available") + logger.info(" Install: https://cli.github.com/") + logger.info(" Falling back to manual instructions") + else: + try: + # Create GitHub Release using gh CLI + gh_command = [ + "gh", "release", "create", f"v{version}", + "--title", f"{name} v{version}", + "--notes-file", release_notes_path, + dest_path + ] + + logger.info(f" Running: {' '.join(gh_command)}") + + result = subprocess.run( + gh_command, + cwd=BASE_DIR, + capture_output=True, + text=True, + timeout=60 + ) + + if result.returncode == 0: + gh_release_created = True + # Extract release URL from output + gh_release_url = result.stdout.strip() + release_metadata["auto_created"] = True + release_metadata["github_release_url"] = gh_release_url + + # Update metadata file with release URL + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(release_metadata, f, indent=2) + + logger.info("✅ GitHub Release created successfully!") + logger.info(f" Release URL: {gh_release_url}") + else: + logger.error("❌ Failed to create GitHub Release") + logger.error(f" Error: {result.stderr}") + logger.info(" Falling back to manual instructions") + + except subprocess.TimeoutExpired: + logger.error("❌ GitHub Release creation timed out") + logger.info(" Falling back to manual instructions") + except Exception as e: + logger.error(f"❌ Error creating GitHub Release: {e}") + logger.info(" Falling back to manual instructions") + + elif auto_create and dry_run: + logger.info("") + logger.info("🤖 Would attempt to create GitHub Release automatically") + logger.info(f" Would run: gh release create v{version} --title \"{name} v{version}\" --notes-file {release_notes_path} {dest_path}") + + # Show summary + if dry_run: + logger.info("✅ Dry run completed successfully") + elif gh_release_created: + logger.info("✅ GitHub Release published successfully") + logger.info(f" Package: {dest_path}") + logger.info(f" Release notes: {release_notes_path}") + logger.info(f" Metadata: {metadata_path}") + logger.info(f" Release URL: {gh_release_url}") + else: + logger.info("✅ GitHub Release prepared successfully") + logger.info(f" Package: {dest_path}") + logger.info(f" Release notes: {release_notes_path}") + logger.info(f" Metadata: {metadata_path}") + logger.info("") + logger.info("📋 Next steps:") + logger.info(f" 1. Review release notes: {release_notes_path}") + logger.info(f" 2. Create GitHub release:") + logger.info(f" gh release create v{version} --title \"{name} v{version}\" \\") + logger.info(f" --notes-file {release_notes_path} {dest_path}") + + return { + "ok": True, + "target": "gh-release", + "package_path": dest_path, + "release_notes_path": release_notes_path, + "metadata_path": metadata_path, + "github_cli_command": release_metadata["github_cli_command"], + "release_metadata": release_metadata, + "github_release_created": gh_release_created, + "github_release_url": gh_release_url, + "dry_run": dry_run + } + + +def load_manifest(manifest_path: str) -> Optional[Dict[str, Any]]: + """ + Load manifest.json to extract metadata. + + Args: + manifest_path: Path to manifest.json + + Returns: + Manifest dictionary or None + """ + if not os.path.exists(manifest_path): + logger.warning(f"⚠️ Manifest not found: {manifest_path}") + return None + + try: + with open(manifest_path, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError as e: + logger.error(f"❌ Error parsing manifest: {e}") + return None + + +def publish_plugin( + package_path: str, + target: str = "local", + expected_sha256: Optional[str] = None, + manifest_path: Optional[str] = None, + marketplace_endpoint: Optional[str] = None, + dry_run: bool = False, + validate_only: bool = False, + auto_create_release: bool = False +) -> Dict[str, Any]: + """ + Publish a bundled plugin package. + + Args: + package_path: Path to the .tar.gz package file + target: Publication target - 'local', 'marketplace', or 'gh-release' + expected_sha256: Expected SHA256 checksum for validation (optional) + manifest_path: Path to manifest.json (optional, auto-detected) + marketplace_endpoint: Marketplace API endpoint for 'marketplace' target (optional) + dry_run: If True, only show what would be done without making changes + validate_only: If True, only validate checksums and exit + auto_create_release: If True and target is 'gh-release', automatically create the release + + Returns: + Dictionary with publication results + """ + logger.info("=" * 80) + logger.info("🚀 BETTY PLUGIN PUBLISH") + logger.info("=" * 80) + logger.info("") + + if dry_run: + logger.info("🔍 DRY RUN MODE ENABLED") + logger.info("") + + if validate_only: + logger.info("🔍 VALIDATE ONLY MODE ENABLED") + logger.info("") + + # Validate inputs + if not os.path.exists(package_path): + error_msg = f"Package file not found: {package_path}" + logger.error(f"❌ {error_msg}") + return { + "ok": False, + "error": error_msg + } + + valid_targets = ["local", "marketplace", "gh-release"] + if target not in valid_targets: + error_msg = f"Invalid target: {target}. Must be one of: {', '.join(valid_targets)}" + logger.error(f"❌ {error_msg}") + return { + "ok": False, + "error": error_msg + } + + # Auto-detect manifest path if not provided + if not manifest_path: + # Try common locations + package_dir = os.path.dirname(package_path) + possible_manifests = [ + os.path.join(package_dir, "manifest.json"), + os.path.join(BASE_DIR, "dist", "manifest.json") + ] + for possible_path in possible_manifests: + if os.path.exists(possible_path): + manifest_path = possible_path + break + + logger.info(f"📦 Package: {package_path}") + logger.info(f"🎯 Target: {target}") + if manifest_path: + logger.info(f"📄 Manifest: {manifest_path}") + logger.info("") + + # Validate checksum + is_valid, checksums = validate_checksum( + package_path, + expected_sha256=expected_sha256, + manifest_path=manifest_path + ) + + if not is_valid: + return { + "ok": False, + "error": "SHA256 checksum validation failed", + "checksums": checksums + } + + logger.info("") + + # If validate_only mode, stop here + if validate_only: + logger.info("=" * 80) + logger.info("✅ VALIDATION SUCCESSFUL") + logger.info("=" * 80) + logger.info("") + logger.info("Package is valid and ready for publication.") + logger.info(f"To publish, run without --validate-only flag:") + logger.info(f" python skills/plugin.publish/plugin_publish.py {package_path} --target={target}") + return { + "ok": True, + "validated": True, + "checksums": checksums, + "package_path": package_path + } + + # Load metadata from manifest + metadata = {} + if manifest_path: + manifest = load_manifest(manifest_path) + if manifest: + metadata = { + "name": manifest.get("name"), + "version": manifest.get("version"), + "description": manifest.get("description"), + "author": manifest.get("author"), + "license": manifest.get("license"), + "homepage": manifest.get("homepage"), + "repository": manifest.get("repository"), + "tags": manifest.get("tags", []), + "betty_version": manifest.get("betty_version") + } + + # Publish based on target + if target == "local": + result = publish_local(package_path, checksums, metadata, dry_run=dry_run) + elif target == "marketplace": + endpoint = marketplace_endpoint or "https://marketplace.claude.ai/api/v1/plugins" + result = publish_marketplace(package_path, checksums, metadata, endpoint, dry_run=dry_run) + elif target == "gh-release": + result = publish_gh_release(package_path, checksums, metadata, dry_run=dry_run, auto_create=auto_create_release) + + logger.info("") + logger.info("=" * 80) + if result.get("ok"): + if dry_run: + logger.info("✅ DRY RUN COMPLETED SUCCESSFULLY") + else: + logger.info("✅ PUBLICATION SUCCESSFUL") + else: + logger.info("❌ PUBLICATION FAILED") + logger.info("=" * 80) + + return result + + +@capture_telemetry(skill_name="plugin.publish", caller="cli") +def main(): + """CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Publish a bundled Betty plugin package to various targets", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Publish to local directory + %(prog)s dist/betty-1.0.0.tar.gz --target=local + + # Publish to Claude Marketplace (simulated) + %(prog)s dist/betty-1.0.0.tar.gz --target=marketplace + + # Prepare GitHub Release + %(prog)s dist/betty-1.0.0.tar.gz --target=gh-release + + # Create GitHub Release automatically + %(prog)s dist/betty-1.0.0.tar.gz --target=gh-release --auto-create + + # Dry run (show what would happen) + %(prog)s dist/betty-1.0.0.tar.gz --target=marketplace --dry-run + + # Validate only (check checksums without publishing) + %(prog)s dist/betty-1.0.0.tar.gz --validate-only + """ + ) + parser.add_argument( + "package_path", + help="Path to the .tar.gz package file" + ) + parser.add_argument( + "--target", + choices=["local", "marketplace", "gh-release"], + default="local", + help="Publication target: local (dist/published/), marketplace (Claude Marketplace API), or gh-release (GitHub Release) (default: local)" + ) + parser.add_argument( + "--sha256", + help="Expected SHA256 checksum for validation (auto-detected from manifest.json if not provided)" + ) + parser.add_argument( + "--manifest", + help="Path to manifest.json (auto-detected if not provided)" + ) + parser.add_argument( + "--endpoint", + help="Marketplace API endpoint for 'marketplace' target (default: https://marketplace.claude.ai/api/v1/plugins)" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be done without making any changes" + ) + parser.add_argument( + "--validate-only", + action="store_true", + help="Only validate package checksums without publishing" + ) + parser.add_argument( + "--auto-create", + action="store_true", + help="For gh-release target: automatically create GitHub Release using gh CLI (requires gh CLI installed)" + ) + + args = parser.parse_args() + + # Validate argument combinations + if args.validate_only and args.dry_run: + logger.warning("⚠️ Both --validate-only and --dry-run specified. Using --validate-only.") + logger.warning(" (--dry-run is ignored when --validate-only is set)") + logger.info("") + + if args.auto_create and args.target != "gh-release": + logger.warning(f"⚠️ --auto-create only applies to gh-release target (current target: {args.target})") + logger.info("") + + result = publish_plugin( + package_path=args.package_path, + target=args.target, + expected_sha256=args.sha256, + manifest_path=args.manifest, + marketplace_endpoint=args.endpoint, + dry_run=args.dry_run, + validate_only=args.validate_only, + auto_create_release=args.auto_create + ) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/plugin.publish/skill.yaml b/skills/plugin.publish/skill.yaml new file mode 100644 index 0000000..3f62e72 --- /dev/null +++ b/skills/plugin.publish/skill.yaml @@ -0,0 +1,88 @@ +name: plugin.publish +version: 0.1.0 +description: > + Publish a bundled plugin package (.tar.gz) to various targets: local directory, + remote Claude Marketplace endpoint, or GitHub Releases. Validates SHA256 checksums + before publication and generates publication metadata for tracking and auditing. + +inputs: + - name: package_path + type: string + required: true + description: Path to the .tar.gz package file built by plugin.build + - name: target + type: string + required: false + default: local + description: "Publication target: 'local' (dist/published/), 'remote' (Claude Marketplace), or 'release' (GitHub Releases)" + - name: expected_sha256 + type: string + required: false + description: Expected SHA256 checksum for validation (optional, auto-detected from manifest.json) + - name: manifest_path + type: string + required: false + description: Path to manifest.json (optional, auto-detected from package directory) + - name: remote_endpoint + type: string + required: false + description: Remote API endpoint URL for 'remote' target (defaults to Claude Marketplace) + +outputs: + - name: publication_result + type: object + description: Publication result with target, paths, and metadata + - name: checksums + type: object + description: Validated MD5 and SHA256 checksums + - name: publication_metadata + type: object + description: Publication metadata including timestamp and target details + +dependencies: + - plugin.build + +status: active + +entrypoints: + - command: /plugin/publish + handler: plugin_publish.py + runtime: python + description: > + Publish a bundled plugin package to local, remote, or release targets. + Validates SHA256 checksums and generates publication metadata. + parameters: + - name: package_path + type: string + required: true + description: Path to the .tar.gz package file + - name: target + type: string + required: false + default: local + description: "Publication target: local, remote, or release" + - name: expected_sha256 + type: string + required: false + description: Expected SHA256 checksum for validation + - name: manifest_path + type: string + required: false + description: Path to manifest.json + - name: remote_endpoint + type: string + required: false + description: Remote API endpoint URL for 'remote' target + permissions: + - filesystem:read + - filesystem:write + - network:http + +tags: + - plugin + - publishing + - deployment + - distribution + - marketplace + - github-releases + - checksum-validation diff --git a/skills/plugin.sync/SKILL.md b/skills/plugin.sync/SKILL.md new file mode 100644 index 0000000..0d9064f --- /dev/null +++ b/skills/plugin.sync/SKILL.md @@ -0,0 +1,504 @@ +--- +name: Plugin Sync +description: Automatically generate plugin.yaml from Betty Framework registries +--- + +# plugin.sync + +## Overview + +**plugin.sync** is the synchronization tool that generates `plugin.yaml` from Betty Framework's registry files. It ensures that Claude Code's plugin configuration stays in sync with registered skills, commands, and hooks. + +## Purpose + +Automates the generation of `plugin.yaml` to maintain consistency between: +- **Skill Registry** (`registry/skills.json`) – Active skills with entrypoints +- **Command Registry** (`registry/commands.json`) – Slash commands +- **Hook Registry** (`registry/hooks.json`) – Event-driven hooks +- **Plugin Configuration** (`plugin.yaml`) – Claude Code plugin manifest + +This eliminates manual editing of `plugin.yaml` and prevents drift between what's registered and what's exposed to Claude Code. + +## What It Does + +1. **Reads Registries**: Loads `skills.json`, `commands.json`, and `hooks.json` +2. **Filters Active Skills**: Processes only skills with `status: active` and defined entrypoints +3. **Validates Handlers**: Checks if handler files exist on disk +4. **Generates Commands**: Converts skill entrypoints to `plugin.yaml` command format +5. **Preserves Metadata**: Maintains existing plugin metadata (author, license, etc.) +6. **Writes plugin.yaml**: Outputs formatted plugin configuration to repo root +7. **Reports Issues**: Warns about missing handlers or inconsistencies + +## Usage + +### Basic Usage + +```bash +python skills/plugin.sync/plugin_sync.py +``` + +No arguments required - reads from standard registry locations. + +### Via Betty CLI + +```bash +/plugin/sync +``` + +### Expected Registry Structure + +The skill expects these registry files: + +``` +betty/ +├── registry/ +│ ├── skills.json # Registered skills +│ ├── commands.json # Registered commands +│ └── hooks.json # Registered hooks +└── plugin.yaml # Generated output +``` + +## Behavior + +### 1. Registry Loading + +Reads JSON files from: +- `registry/skills.json` +- `registry/commands.json` +- `registry/hooks.json` + +If a registry file is missing, logs a warning and continues with available data. + +### 2. Skill Processing + +For each skill in `skills.json`: +- **Checks status**: Only processes skills with `status: active` +- **Looks for entrypoints**: Requires at least one entrypoint definition +- **Validates handler**: Checks if handler file exists at `skills/{skill_name}/{handler}` +- **Converts format**: Maps skill entrypoint to plugin command schema + +### 3. Command Generation + +Creates a command entry for each active skill entrypoint: + +```yaml +- name: skill/validate + description: Validate a skill manifest + handler: + runtime: python + script: skills/skill.define/skill_define.py + parameters: + - name: manifest_path + type: string + required: true + description: Path to skill.yaml file + permissions: + - filesystem:read + - filesystem:write +``` + +### 4. Handler Validation + +For each handler reference: +- Constructs full path: `skills/{skill_name}/{handler_filename}` +- Checks file existence on disk +- Logs warning if handler file is missing + +### 5. Plugin Generation + +Preserves existing `plugin.yaml` metadata: +- Plugin name, version, description +- Author information +- License +- Requirements +- Permissions +- Config sections + +Replaces the `commands` section with generated entries. + +### 6. Output Writing + +Writes `plugin.yaml` with: +- Auto-generated header comment +- Properly formatted YAML (2-space indent) +- Generation timestamp in metadata +- Skill and command counts + +## Outputs + +### Success Response + +```json +{ + "ok": true, + "status": "success", + "output_path": "/home/user/betty/plugin.yaml", + "commands_generated": 18, + "warnings": [] +} +``` + +### Response with Warnings + +```json +{ + "ok": true, + "status": "success", + "output_path": "/home/user/betty/plugin.yaml", + "commands_generated": 18, + "warnings": [ + "Handler not found for 'api.validate': skills/api.validate/api_validate_missing.py", + "Skill 'test.broken' has entrypoint without handler" + ] +} +``` + +### Failure Response + +```json +{ + "ok": false, + "status": "failed", + "error": "Failed to parse JSON from registry/skills.json: Expecting value: line 1 column 1" +} +``` + +## Generated plugin.yaml Structure + +The skill generates a `plugin.yaml` like this: + +```yaml +# Betty Framework - Claude Code Plugin +# Auto-generated by plugin.sync skill +# DO NOT EDIT MANUALLY - Run plugin.sync to regenerate + +name: betty-framework +version: 1.0.0 +description: Betty Framework - Structured AI-assisted engineering +author: + name: RiskExec + email: platform@riskexec.com + url: https://github.com/epieczko/betty +license: MIT + +metadata: + homepage: https://github.com/epieczko/betty + repository: https://github.com/epieczko/betty + generated_at: "2025-10-23T17:45:00.123456+00:00" + generated_by: plugin.sync skill + skill_count: 18 + command_count: 18 + +requirements: + python: ">=3.11" + packages: + - pyyaml + +permissions: + - filesystem:read + - filesystem:write + - process:execute + +commands: + - name: workflow/validate + description: Validates Betty workflow YAML definitions + handler: + runtime: python + script: skills/workflow.validate/workflow_validate.py + parameters: + - name: workflow.yaml + type: string + required: true + description: Path to the workflow YAML file + permissions: + - filesystem + - read + + - name: skill/define + description: Validate a Claude Code skill manifest + handler: + runtime: python + script: skills/skill.define/skill_define.py + parameters: + - name: manifest_path + type: string + required: true + description: Path to the skill.yaml file + permissions: + - filesystem + - read + - write + + # ... more commands ... +``` + +## Validation and Warnings + +### Handler Existence Check + +For each skill entrypoint, the skill checks: + +```python +full_path = f"skills/{skill_name}/{handler_filename}" +if not os.path.exists(full_path): + warnings.append(f"Handler not found: {full_path}") +``` + +### Common Warnings + +| Warning | Meaning | Action | +|---------|---------|--------| +| `Handler not found for 'X'` | Handler file missing from disk | Create the handler or fix the path in skill.yaml | +| `Skill 'X' has entrypoint without handler` | Entrypoint missing `handler` field | Add handler field to entrypoint definition | +| `Registry file not found` | Registry JSON is missing | Run registry update or check file paths | + +## Examples + +### Example 1: Full Sync After Adding New Skills + +**Scenario**: You've added several new skills and want to sync them to `plugin.yaml` + +```bash +# Create skills using skill.create +/skill/create data.transform "Transform data between formats" +/skill/create api.monitor "Monitor API health" --inputs=endpoint --outputs=status + +# Define skills (validates and registers) +/skill/define skills/data.transform/skill.yaml +/skill/define skills/api.monitor/skill.yaml + +# Sync to plugin.yaml +/plugin/sync +``` + +**Output**: +``` +INFO: Starting plugin.yaml generation from registries... +INFO: Loading registry files... +INFO: Generating plugin.yaml configuration... +INFO: Added command: /data/transform from skill data.transform +INFO: Added command: /api/monitor from skill api.monitor +INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml +INFO: ✅ Generated 20 commands +INFO: 📄 Output: /home/user/betty/plugin.yaml +``` + +### Example 2: Detecting Missing Handlers + +**Scenario**: A skill's handler file was moved or deleted + +```bash +# Remove a handler file +rm skills/api.validate/api_validate.py + +# Run sync +/plugin/sync +``` + +**Output**: +``` +INFO: Starting plugin.yaml generation from registries... +INFO: Loading registry files... +INFO: Generating plugin.yaml configuration... +INFO: Added command: /skill/define from skill skill.define +WARNING: Handler not found for 'api.validate': skills/api.validate/api_validate.py +INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml +INFO: ⚠️ Warnings during generation: +INFO: - Handler not found for 'api.validate': skills/api.validate/api_validate.py +INFO: ✅ Generated 19 commands +``` + +### Example 3: Initial Plugin Setup + +**Scenario**: Setting up Betty Framework plugin for the first time + +```bash +# Initialize registry if needed +python -c "from betty.config import ensure_directories; ensure_directories()" + +# Register all existing skills +for skill in skills/*/skill.yaml; do + /skill/define "$skill" +done + +# Generate plugin.yaml +/plugin/sync +``` + +This creates a complete `plugin.yaml` from all registered active skills. + +## Integration + +### With skill.define + +After defining a skill, sync the plugin: + +```bash +/skill/define skills/my.skill/skill.yaml +/plugin/sync +``` + +### With Workflows + +Include plugin sync as a workflow step: + +```yaml +# workflows/skill_lifecycle.yaml +steps: + - skill: skill.create + args: ["new.skill", "Description"] + + - skill: skill.define + args: ["skills/new.skill/skill.yaml"] + + - skill: plugin.sync + args: [] +``` + +### With Hooks + +Auto-sync when skills are updated: + +```yaml +# .claude/hooks.yaml +- event: on_file_save + pattern: "skills/*/skill.yaml" + command: python skills/skill.define/skill_define.py {file_path} && python skills/plugin.sync/plugin_sync.py + blocking: false + description: Auto-sync plugin.yaml when skills change +``` + +## What Gets Included + +### ✅ Included in plugin.yaml + +- Skills with `status: active` +- Skills with at least one `entrypoint` defined +- All entrypoint parameters and permissions +- Skill descriptions and metadata + +### ❌ Not Included + +- Skills with `status: draft` +- Skills without entrypoints +- Skills marked as internal-only +- Test skills (unless marked active) + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "Failed to parse JSON" | Invalid JSON in registry file | Fix JSON syntax in the registry | +| "Registry file not found" | Missing registry file | Ensure registries exist in `registry/` dir | +| "Permission denied" | Cannot write plugin.yaml | Check file permissions on plugin.yaml | +| All commands missing | No active skills | Mark skills as active in registry | + +## Files Read + +- `registry/skills.json` – Skill registry +- `registry/commands.json` – Command registry (future use) +- `registry/hooks.json` – Hook registry (future use) +- `plugin.yaml` – Existing plugin config (for metadata preservation) +- `skills/*/` – Handler file validation + +## Files Modified + +- `plugin.yaml` – Overwritten with generated configuration + +## Exit Codes + +- **0**: Success (plugin.yaml generated successfully) +- **1**: Failure (error during generation) + +## Logging + +Logs generation progress: + +``` +INFO: Starting plugin.yaml generation from registries... +INFO: Loading registry files... +INFO: Loaded existing plugin.yaml as template +INFO: Generating plugin.yaml configuration... +INFO: Added command: /skill/create from skill skill.create +INFO: Added command: /skill/define from skill skill.define +INFO: Added command: /agent/define from skill agent.define +INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml +INFO: ✅ Generated 18 commands +INFO: 📄 Output: /home/user/betty/plugin.yaml +``` + +## Best Practices + +1. **Run After Registry Changes**: Sync after adding, updating, or removing skills +2. **Include in CI/CD**: Add plugin sync to your deployment pipeline +3. **Review Before Commit**: Check generated plugin.yaml before committing +4. **Keep Registries Clean**: Remove inactive skills to keep plugin.yaml focused +5. **Use Hooks**: Set up auto-sync hooks for convenience +6. **Version Control**: Always commit plugin.yaml changes with skill changes + +## Troubleshooting + +### Plugin.yaml Not Updating + +**Problem**: Changes to skills.json don't appear in plugin.yaml + +**Solutions**: +- Ensure skill status is `active` +- Check that skill has `entrypoints` defined +- Verify entrypoint has `command` and `handler` fields +- Run `/skill/define` before `/plugin/sync` + +### Handler Not Found Warnings + +**Problem**: Warnings about missing handler files + +**Solutions**: +- Check handler path in skill.yaml +- Ensure handler file exists at `skills/{skill_name}/{handler_filename}` +- Verify file permissions +- Update skill.yaml if handler was renamed + +### Commands Not Appearing + +**Problem**: Active skill not generating command + +**Solutions**: +- Verify skill has `entrypoints` array in skill.yaml +- Check entrypoint has `command` field (e.g., `/skill/validate`) +- Ensure `handler` field points to correct file +- Check that skill.yaml is in skills registry + +## Architecture + +### Skill Categories + +**Infrastructure** – Plugin.sync maintains the foundation layer by syncing registry state to plugin configuration. + +### Design Principles + +- **Single Source of Truth**: Registry files are the source of truth +- **Idempotent**: Can be run multiple times safely +- **Validation First**: Checks handlers before generating config +- **Preserve Metadata**: Keeps existing plugin metadata intact +- **Clear Reporting**: Detailed warnings about issues + +## See Also + +- **skill.define** – Validate and register skills ([SKILL.md](../skill.define/SKILL.md)) +- **registry.update** – Update skill registry ([SKILL.md](../registry.update/SKILL.md)) +- **skill.create** – Create new skills ([SKILL.md](../skill.create/SKILL.md)) +- **Betty Architecture** – Framework overview ([betty-architecture.md](../../docs/betty-architecture.md)) + +## Dependencies + +- **registry.update**: Registry management +- **betty.config**: Configuration constants and paths +- **betty.logging_utils**: Logging infrastructure + +## Status + +**Active** – Production-ready infrastructure skill + +## Version History + +- **0.1.0** (Oct 2025) – Initial implementation with registry sync and handler validation diff --git a/skills/plugin.sync/__init__.py b/skills/plugin.sync/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/plugin.sync/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/plugin.sync/plugin_sync.py b/skills/plugin.sync/plugin_sync.py new file mode 100755 index 0000000..5859b5a --- /dev/null +++ b/skills/plugin.sync/plugin_sync.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +""" +plugin_sync.py – Implementation of the plugin.sync Skill +Generates plugin.yaml from registry files (skills.json, commands.json, hooks.json). +""" + +import os +import sys +import json +import yaml +from typing import Dict, Any, List, Optional +from datetime import datetime, timezone +from pathlib import Path + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + + +def load_registry_file(registry_path: str) -> Dict[str, Any]: + """ + Load a JSON registry file. + + Args: + registry_path: Path to the registry JSON file + + Returns: + Parsed registry data + + Raises: + FileNotFoundError: If registry file doesn't exist + json.JSONDecodeError: If JSON is invalid + """ + try: + with open(registry_path) as f: + return json.load(f) + except FileNotFoundError: + logger.warning(f"Registry file not found: {registry_path}") + return {} + except json.JSONDecodeError as e: + logger.error(f"Failed to parse JSON from {registry_path}: {e}") + raise + + +def check_handler_exists(handler_path: str, skill_name: str) -> Dict[str, Any]: + """ + Check if a handler file exists on disk. + + Args: + handler_path: Relative path to handler file + skill_name: Name of the skill + + Returns: + Dictionary with exists flag and full path + """ + full_path = os.path.join(BASE_DIR, "skills", skill_name, handler_path) + exists = os.path.exists(full_path) + + return { + "exists": exists, + "path": full_path, + "relative_path": f"skills/{skill_name}/{handler_path}" + } + + +def convert_skill_to_command(skill: Dict[str, Any], entrypoint: Dict[str, Any]) -> Dict[str, Any]: + """ + Convert a skill registry entry with entrypoint to a plugin.yaml command format. + + Args: + skill: Skill entry from skills.json + entrypoint: Entrypoint entry from the skill + + Returns: + Command dictionary in plugin.yaml format + """ + # Extract command name (remove leading slash if present) + command_name = entrypoint.get("command", "").lstrip("/") + + # Build handler section + handler_path = f"skills/{skill['name']}/{entrypoint.get('handler', '')}" + + command = { + "name": command_name, + "description": entrypoint.get("description") or skill.get("description", ""), + "handler": { + "runtime": entrypoint.get("runtime", "python"), + "script": handler_path + } + } + + # Add parameters from entrypoint or extract from skill inputs + if "parameters" in entrypoint and entrypoint["parameters"]: + # Use parameters from entrypoint if they exist + command["parameters"] = entrypoint["parameters"] + elif "inputs" in skill and skill["inputs"]: + # Otherwise, convert skill inputs to command parameters + parameters = [] + for inp in skill["inputs"]: + if isinstance(inp, dict): + # Full input specification + param = { + "name": inp.get("name", ""), + "type": inp.get("type", "string"), + "required": inp.get("required", False), + "description": inp.get("description", "") + } + # Add default if present + if "default" in inp: + param["default"] = inp["default"] + parameters.append(param) + elif isinstance(inp, str): + # Simple string input (legacy format) + parameters.append({ + "name": inp, + "type": "string", + "required": False, + "description": "" + }) + if parameters: + command["parameters"] = parameters + + # Add permissions if present + if "permissions" in entrypoint: + # Convert permissions list to proper format if needed + permissions = entrypoint["permissions"] + if isinstance(permissions, list): + command["permissions"] = permissions + + return command + + +def generate_plugin_yaml( + skills_data: Dict[str, Any], + commands_data: Dict[str, Any], + hooks_data: Dict[str, Any] +) -> tuple[Dict[str, Any], List[str]]: + """ + Generate plugin.yaml content from registry data. + + Args: + skills_data: Parsed skills.json + commands_data: Parsed commands.json + hooks_data: Parsed hooks.json + + Returns: + Tuple of (plugin_yaml_dict, list of warnings) + """ + warnings = [] + commands = [] + + # Load existing plugin.yaml to preserve header content + plugin_yaml_path = os.path.join(BASE_DIR, "plugin.yaml") + base_config = {} + + try: + with open(plugin_yaml_path) as f: + base_config = yaml.safe_load(f) or {} + logger.info(f"Loaded existing plugin.yaml as template") + except FileNotFoundError: + logger.warning("No existing plugin.yaml found, creating from scratch") + base_config = { + "name": "betty-framework", + "version": "1.0.0", + "description": "Betty Framework - Structured AI-assisted engineering", + "author": { + "name": "RiskExec", + "email": "platform@riskexec.com", + "url": "https://github.com/epieczko/betty" + }, + "license": "MIT" + } + + # Process active skills with entrypoints + skills = skills_data.get("skills", []) + for skill in skills: + if skill.get("status") != "active": + continue + + entrypoints = skill.get("entrypoints", []) + if not entrypoints: + continue + + skill_name = skill.get("name") + + for entrypoint in entrypoints: + handler = entrypoint.get("handler") + if not handler: + warnings.append(f"Skill '{skill_name}' has entrypoint without handler") + continue + + # Check if handler exists on disk + handler_check = check_handler_exists(handler, skill_name) + if not handler_check["exists"]: + warnings.append( + f"Handler not found for '{skill_name}': {handler_check['relative_path']}" + ) + + # Convert to plugin command format + command = convert_skill_to_command(skill, entrypoint) + commands.append(command) + + logger.info(f"Added command: /{command['name']} from skill {skill_name}") + + # Process commands from commands.json (if any need to be added) + # Note: Most commands should already be represented via skills + # This is mainly for custom commands that don't map to skills + registry_commands = commands_data.get("commands", []) + for cmd in registry_commands: + if cmd.get("status") == "active": + # Check if this command is already in our list + cmd_name = cmd.get("name", "").lstrip("/") + if not any(c["name"] == cmd_name for c in commands): + logger.info(f"Command '{cmd_name}' in registry but no matching active skill found") + + # Build final plugin.yaml structure + plugin_config = { + **base_config, + "commands": commands + } + + # Override plugin-level permissions (remove network:none contradiction) + plugin_config["permissions"] = [ + "filesystem:read", + "filesystem:write", + "process:execute" + ] + + # Add metadata about generation + if "metadata" not in plugin_config: + plugin_config["metadata"] = {} + + plugin_config["metadata"]["generated_at"] = datetime.now(timezone.utc).isoformat() + plugin_config["metadata"]["generated_by"] = "plugin.sync skill" + plugin_config["metadata"]["skill_count"] = len([s for s in skills if s.get("status") == "active"]) + plugin_config["metadata"]["command_count"] = len(commands) + + return plugin_config, warnings + + +def write_plugin_yaml(plugin_config: Dict[str, Any], output_path: str): + """ + Write plugin.yaml to disk with proper formatting. + + Args: + plugin_config: Plugin configuration dictionary + output_path: Path where to write plugin.yaml + """ + # Add header comment + header = """# Betty Framework - Claude Code Plugin +# Auto-generated by plugin.sync skill +# DO NOT EDIT MANUALLY - Run plugin.sync to regenerate + +""" + + with open(output_path, 'w') as f: + f.write(header) + yaml.dump(plugin_config, f, default_flow_style=False, sort_keys=False, indent=2) + + logger.info(f"✅ Written plugin.yaml to {output_path}") + + +def main(): + """Main CLI entry point.""" + logger.info("Starting plugin.yaml generation from registries...") + + # Define registry paths + skills_path = os.path.join(BASE_DIR, "registry", "skills.json") + commands_path = os.path.join(BASE_DIR, "registry", "commands.json") + hooks_path = os.path.join(BASE_DIR, "registry", "hooks.json") + + try: + # Load registry files + logger.info("Loading registry files...") + skills_data = load_registry_file(skills_path) + commands_data = load_registry_file(commands_path) + hooks_data = load_registry_file(hooks_path) + + # Generate plugin.yaml content + logger.info("Generating plugin.yaml configuration...") + plugin_config, warnings = generate_plugin_yaml(skills_data, commands_data, hooks_data) + + # Write to file + output_path = os.path.join(BASE_DIR, "plugin.yaml") + write_plugin_yaml(plugin_config, output_path) + + # Report results + result = { + "ok": True, + "status": "success", + "output_path": output_path, + "commands_generated": len(plugin_config.get("commands", [])), + "warnings": warnings + } + + # Print warnings if any + if warnings: + logger.warning("⚠️ Warnings during generation:") + for warning in warnings: + logger.warning(f" - {warning}") + + # Print summary + logger.info(f"✅ Generated {result['commands_generated']} commands") + logger.info(f"📄 Output: {output_path}") + + print(json.dumps(result, indent=2)) + sys.exit(0) + + except Exception as e: + logger.error(f"Failed to generate plugin.yaml: {e}") + result = { + "ok": False, + "status": "failed", + "error": str(e) + } + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/plugin.sync/skill.yaml b/skills/plugin.sync/skill.yaml new file mode 100644 index 0000000..4139c60 --- /dev/null +++ b/skills/plugin.sync/skill.yaml @@ -0,0 +1,29 @@ +name: plugin.sync +version: 0.1.0 +description: > + Automatically generates plugin.yaml from Betty Framework registries. + Reads skills.json, commands.json, and hooks.json to build a complete plugin configuration. +inputs: [] +outputs: + - plugin.yaml + - generation_report.json +dependencies: + - registry.update +status: active + +entrypoints: + - command: /plugin/sync + handler: plugin_sync.py + runtime: python + description: > + Generate plugin.yaml from registry files. Syncs active skills with entrypoints into plugin commands. + parameters: [] + permissions: + - filesystem:read + - filesystem:write + +tags: + - plugin + - registry + - automation + - infrastructure diff --git a/skills/policy.enforce/SKILL.md b/skills/policy.enforce/SKILL.md new file mode 100644 index 0000000..14db47b --- /dev/null +++ b/skills/policy.enforce/SKILL.md @@ -0,0 +1,310 @@ +--- +name: policy.enforce +description: Enforces policy rules for skill and agent manifests +--- + +# policy.enforce + +Enforces policy rules for skill and agent manifests including naming conventions, semantic versioning, permissions validation, and status lifecycle checks. + +## Overview + +The `policy.enforce` skill validates skill and agent manifests against Betty Framework policy rules to ensure consistency and compliance across the framework. It supports both single-file validation and batch mode for scanning all manifests. + +## Policy Rules + +### 1. Naming Convention +- Names must be **lowercase only** +- Can use **dots** for namespacing (e.g., `api.define`, `workflow.compose`) +- Must start with a lowercase letter +- Must end with a letter or number +- **No spaces, underscores, or hyphens** in the name field + +**Valid Examples:** +- `api.define` +- `policy.enforce` +- `skill.create` + +**Invalid Examples:** +- `API.define` (uppercase) +- `api_define` (underscore) +- `api define` (space) +- `api-define` (hyphen) + +### 2. Semantic Versioning +- Must follow semantic versioning format: `MAJOR.MINOR.PATCH` +- Optional pre-release suffix allowed (e.g., `1.0.0-alpha`) + +**Valid Examples:** +- `0.1.0` +- `1.0.0` +- `2.3.1-beta` + +**Invalid Examples:** +- `1.0` (missing patch) +- `v1.0.0` (prefix not allowed) +- `1.0.0.0` (too many segments) + +### 3. Permissions +Only the following permissions are allowed: +- `filesystem` (or scoped: `filesystem:read`, `filesystem:write`) +- `network` (or scoped: `network:http`, `network:https`) +- `read` +- `write` + +Any other permissions will trigger a violation. + +### 4. Status +Must be one of: +- `draft` - Under development +- `active` - Production ready +- `deprecated` - Still works but not recommended +- `archived` - No longer maintained + +## Usage + +### Single File Validation + +Validate a single skill or agent manifest: + +```bash +python3 skills/policy.enforce/policy_enforce.py path/to/skill.yaml +``` + +Example: +```bash +python3 skills/policy.enforce/policy_enforce.py skills/api.define/skill.yaml +``` + +### Batch Mode + +Validate all manifests in `skills/` and `agents/` directories: + +```bash +python3 skills/policy.enforce/policy_enforce.py --batch +``` + +Or simply run without arguments: +```bash +python3 skills/policy.enforce/policy_enforce.py +``` + +### Strict Mode + +Treat warnings as errors (if warnings are implemented in future): + +```bash +python3 skills/policy.enforce/policy_enforce.py --batch --strict +``` + +## Output Format + +The skill outputs JSON with the following structure: + +### Single File Success + +```json +{ + "success": true, + "path": "skills/api.define/skill.yaml", + "manifest_type": "skill", + "violations": [], + "violation_count": 0, + "message": "All policy checks passed" +} +``` + +### Single File Failure + +```json +{ + "success": false, + "path": "skills/example/skill.yaml", + "manifest_type": "skill", + "violations": [ + { + "field": "name", + "rule": "naming_convention", + "message": "Name contains uppercase letters: 'API.define'. Names must be lowercase.", + "severity": "error" + }, + { + "field": "version", + "rule": "semantic_versioning", + "message": "Invalid version: '1.0'. Must follow semantic versioning (e.g., 1.0.0, 0.1.0-alpha)", + "severity": "error" + } + ], + "violation_count": 2, + "message": "Found 2 policy violation(s)" +} +``` + +### Batch Mode + +```json +{ + "success": true, + "mode": "batch", + "message": "Validated 15 manifest(s): 15 passed, 0 failed", + "total_manifests": 15, + "passed": 15, + "failed": 0, + "results": [ + { + "success": true, + "path": "skills/api.define/skill.yaml", + "manifest_type": "skill", + "violations": [], + "violation_count": 0, + "message": "All policy checks passed" + } + ] +} +``` + +## Interpreting Results + +### Exit Codes +- `0` - All policy checks passed +- `1` - One or more policy violations found + +### Violation Severity +- `error` - Blocking violation that must be fixed +- `warning` - Non-blocking issue (recommended to fix) + +### Common Violations + +**Naming Convention Violations:** +- Use lowercase: `API.define` → `api.define` +- Use dots instead of underscores: `api_define` → `api.define` +- Remove spaces: `api define` → `api.define` + +**Version Violations:** +- Add patch version: `1.0` → `1.0.0` +- Remove prefix: `v1.0.0` → `1.0.0` + +**Permission Violations:** +- Use only allowed permissions +- Replace `http` with `network:http` +- Replace `file` with `filesystem` + +**Status Violations:** +- Use lowercase: `Active` → `active` +- Use valid values: `production` → `active` + +## Integration with CI/CD + +Add to your CI pipeline to enforce policies on all manifests: + +```yaml +# .github/workflows/policy-check.yml +name: Policy Enforcement + +on: [push, pull_request] + +jobs: + policy-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run Policy Enforcement + run: | + python3 skills/policy.enforce/policy_enforce.py --batch +``` + +## Inputs + +| Input | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `manifest_path` | string | No | - | Path to a single manifest file to validate | +| `--batch` | boolean | No | false | Enable batch mode to validate all manifests | +| `--strict` | boolean | No | false | Treat warnings as errors | + +## Outputs + +| Output | Type | Description | +|--------|------|-------------| +| `success` | boolean | Whether all policy checks passed | +| `violations` | array | List of policy violations found | +| `violation_count` | integer | Total number of violations | +| `message` | string | Summary message | +| `path` | string | Path to the validated manifest (single mode) | +| `manifest_type` | string | Type of manifest: "skill" or "agent" (single mode) | +| `total_manifests` | integer | Total number of manifests checked (batch mode) | +| `passed` | integer | Number of manifests that passed (batch mode) | +| `failed` | integer | Number of manifests that failed (batch mode) | +| `results` | array | Individual results for each manifest (batch mode) | + +## Dependencies + +- `betty.config` - Configuration and paths +- `betty.validation` - Validation utilities +- `betty.logging_utils` - Logging infrastructure +- PyYAML - YAML parsing + +## Status + +**Active** - Production ready + +## Examples + +### Example 1: Validate a Single Skill + +```bash +$ python3 skills/policy.enforce/policy_enforce.py skills/api.define/skill.yaml +{ + "success": true, + "path": "skills/api.define/skill.yaml", + "manifest_type": "skill", + "violations": [], + "violation_count": 0, + "message": "All policy checks passed" +} +``` + +### Example 2: Batch Validation + +```bash +$ python3 skills/policy.enforce/policy_enforce.py --batch +{ + "success": true, + "mode": "batch", + "message": "Validated 15 manifest(s): 15 passed, 0 failed", + "total_manifests": 15, + "passed": 15, + "failed": 0, + "results": [...] +} +``` + +### Example 3: Failed Validation + +```bash +$ python3 skills/policy.enforce/policy_enforce.py skills/bad-example/skill.yaml +{ + "success": false, + "path": "skills/bad-example/skill.yaml", + "manifest_type": "skill", + "violations": [ + { + "field": "name", + "rule": "naming_convention", + "message": "Name contains uppercase letters: 'Bad-Example'. Names must be lowercase.", + "severity": "error" + } + ], + "violation_count": 1, + "message": "Found 1 policy violation(s)" +} +$ echo $? +1 +``` + +## Future Enhancements + +- Custom policy rule definitions via YAML files +- Support for warning-level violations +- Auto-fix capability for common violations +- Policy exemptions and overrides +- Historical violation tracking diff --git a/skills/policy.enforce/__init__.py b/skills/policy.enforce/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/policy.enforce/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/policy.enforce/policy_enforce.py b/skills/policy.enforce/policy_enforce.py new file mode 100755 index 0000000..8255a0c --- /dev/null +++ b/skills/policy.enforce/policy_enforce.py @@ -0,0 +1,449 @@ +#!/usr/bin/env python3 +""" +policy_enforce.py - Implementation of the policy.enforce Skill + +Enforces policy rules for skill and agent manifests including: +- Naming conventions (lowercase, dot-separated, no spaces) +- Semantic versioning +- Permissions validation (only filesystem, network, read, write) +- Status lifecycle checks (draft, active, deprecated, archived) + +Supports both single-file validation and batch mode. +""" + +import os +import sys +import json +import yaml +import re +from typing import Dict, Any, List, Optional, Tuple +from pathlib import Path + + +from betty.config import BASE_DIR, SKILLS_DIR, AGENTS_DIR +from betty.validation import validate_path, validate_version, ValidationError +from betty.logging_utils import setup_logger + +logger = setup_logger(__name__) + +# Policy definitions +ALLOWED_PERMISSIONS = {"filesystem", "network", "read", "write"} +ALLOWED_STATUSES = {"draft", "active", "deprecated", "archived"} +VALID_NAME_PATTERN = r"^[a-z][a-z0-9.]*[a-z0-9]$" # lowercase, dots allowed, no spaces + + +class PolicyViolation: + """Represents a single policy violation.""" + + def __init__(self, field: str, rule: str, message: str, severity: str = "error"): + self.field = field + self.rule = rule + self.message = message + self.severity = severity # "error" or "warning" + + def to_dict(self) -> Dict[str, str]: + return { + "field": self.field, + "rule": self.rule, + "message": self.message, + "severity": self.severity + } + + +def load_manifest(path: str) -> Dict[str, Any]: + """ + Load and parse a manifest from YAML file. + + Args: + path: Path to manifest file + + Returns: + Parsed manifest dictionary + + Raises: + Exception: If manifest cannot be loaded or parsed + """ + try: + with open(path) as f: + manifest = yaml.safe_load(f) + return manifest + except FileNotFoundError: + raise Exception(f"Manifest file not found: {path}") + except yaml.YAMLError as e: + raise Exception(f"Failed to parse YAML: {e}") + + +def validate_name_format(name: str) -> Optional[PolicyViolation]: + """ + Validate that name follows naming convention: + - lowercase letters and numbers only + - dots allowed for namespacing + - no spaces, underscores, or hyphens (except in skill directory names for backwards compatibility) + + Args: + name: Name to validate + + Returns: + PolicyViolation if invalid, None if valid + """ + if not name: + return PolicyViolation( + field="name", + rule="naming_convention", + message="Name cannot be empty" + ) + + # Check for spaces + if ' ' in name: + return PolicyViolation( + field="name", + rule="naming_convention", + message=f"Name contains spaces: '{name}'. Names must not contain spaces." + ) + + # Check for uppercase + if name != name.lower(): + return PolicyViolation( + field="name", + rule="naming_convention", + message=f"Name contains uppercase letters: '{name}'. Names must be lowercase." + ) + + # Check format: lowercase, dots allowed, must start with letter + if not re.match(VALID_NAME_PATTERN, name): + return PolicyViolation( + field="name", + rule="naming_convention", + message=f"Invalid name format: '{name}'. " + "Names must start with a lowercase letter, contain only lowercase letters, " + "numbers, and dots for namespacing, and end with a letter or number." + ) + + return None + + +def validate_version_format(version: str) -> Optional[PolicyViolation]: + """ + Validate that version follows semantic versioning. + + Args: + version: Version string to validate + + Returns: + PolicyViolation if invalid, None if valid + """ + try: + validate_version(version) + return None + except ValidationError as e: + return PolicyViolation( + field="version", + rule="semantic_versioning", + message=str(e) + ) + + +def validate_permissions(manifest: Dict[str, Any], manifest_type: str) -> List[PolicyViolation]: + """ + Validate that all permissions are in the allowed set. + + Args: + manifest: Manifest dictionary + manifest_type: "skill" or "agent" + + Returns: + List of PolicyViolations for invalid permissions + """ + violations = [] + + # For skills, check entrypoints permissions + if manifest_type == "skill": + entrypoints = manifest.get("entrypoints", []) + for idx, entrypoint in enumerate(entrypoints): + permissions = entrypoint.get("permissions", []) + for perm in permissions: + # Handle both simple permissions and scoped permissions (e.g., "filesystem:read") + base_perm = perm.split(':')[0] if ':' in perm else perm + + if base_perm not in ALLOWED_PERMISSIONS: + violations.append(PolicyViolation( + field=f"entrypoints[{idx}].permissions", + rule="allowed_permissions", + message=f"Invalid permission: '{perm}'. " + f"Only {', '.join(sorted(ALLOWED_PERMISSIONS))} are allowed." + )) + + # For agents, permissions might be in a different location + # (checking if there's a permissions field at the top level) + elif manifest_type == "agent": + permissions = manifest.get("permissions", []) + for perm in permissions: + base_perm = perm.split(':')[0] if ':' in perm else perm + + if base_perm not in ALLOWED_PERMISSIONS: + violations.append(PolicyViolation( + field="permissions", + rule="allowed_permissions", + message=f"Invalid permission: '{perm}'. " + f"Only {', '.join(sorted(ALLOWED_PERMISSIONS))} are allowed." + )) + + return violations + + +def validate_status(status: str) -> Optional[PolicyViolation]: + """ + Validate that status is one of the allowed values. + + Args: + status: Status string to validate + + Returns: + PolicyViolation if invalid, None if valid + """ + if not status: + return PolicyViolation( + field="status", + rule="allowed_status", + message="Status field is required" + ) + + if status not in ALLOWED_STATUSES: + return PolicyViolation( + field="status", + rule="allowed_status", + message=f"Invalid status: '{status}'. " + f"Must be one of: {', '.join(sorted(ALLOWED_STATUSES))}" + ) + + return None + + +def validate_manifest_policies(path: str, manifest_type: str = None) -> Dict[str, Any]: + """ + Validate a single manifest against all policy rules. + + Args: + path: Path to manifest file + manifest_type: "skill" or "agent" (auto-detected if None) + + Returns: + Dictionary with validation results + """ + violations = [] + + try: + # Validate path + validate_path(path, must_exist=True) + + # Load manifest + manifest = load_manifest(path) + + # Auto-detect manifest type if not specified + if manifest_type is None: + if "skill.yaml" in path or path.endswith(".yaml") and "skills/" in path: + manifest_type = "skill" + elif "agent.yaml" in path or path.endswith(".yaml") and "agents/" in path: + manifest_type = "agent" + else: + # Try to detect from content + if "entrypoints" in manifest or "inputs" in manifest: + manifest_type = "skill" + elif "capabilities" in manifest or "reasoning_mode" in manifest: + manifest_type = "agent" + else: + manifest_type = "unknown" + + # Validate name + name = manifest.get("name") + if name: + violation = validate_name_format(name) + if violation: + violations.append(violation) + else: + violations.append(PolicyViolation( + field="name", + rule="required_field", + message="Name field is required" + )) + + # Validate version + version = manifest.get("version") + if version: + violation = validate_version_format(version) + if violation: + violations.append(violation) + else: + violations.append(PolicyViolation( + field="version", + rule="required_field", + message="Version field is required" + )) + + # Validate permissions + perm_violations = validate_permissions(manifest, manifest_type) + violations.extend(perm_violations) + + # Validate status + status = manifest.get("status") + violation = validate_status(status) + if violation: + violations.append(violation) + + # Build result + success = len(violations) == 0 + + result = { + "success": success, + "path": path, + "manifest_type": manifest_type, + "violations": [v.to_dict() for v in violations], + "violation_count": len(violations) + } + + if success: + result["message"] = "All policy checks passed" + else: + result["message"] = f"Found {len(violations)} policy violation(s)" + + return result + + except Exception as e: + logger.error(f"Error validating manifest {path}: {e}") + return { + "success": False, + "path": path, + "error": str(e), + "violations": [], + "violation_count": 0 + } + + +def find_all_manifests() -> List[Tuple[str, str]]: + """ + Find all skill and agent manifests in the repository. + + Returns: + List of tuples (path, type) where type is "skill" or "agent" + """ + manifests = [] + + # Find skill manifests + skills_dir = Path(SKILLS_DIR) + if skills_dir.exists(): + for skill_yaml in skills_dir.glob("*/skill.yaml"): + manifests.append((str(skill_yaml), "skill")) + + # Find agent manifests + agents_dir = Path(AGENTS_DIR) + if agents_dir.exists(): + for agent_yaml in agents_dir.glob("*/agent.yaml"): + manifests.append((str(agent_yaml), "agent")) + + return manifests + + +def validate_batch(strict: bool = False) -> Dict[str, Any]: + """ + Validate all manifests in batch mode. + + Args: + strict: If True, treat warnings as errors + + Returns: + Dictionary with batch validation results + """ + manifests = find_all_manifests() + + if not manifests: + return { + "success": True, + "mode": "batch", + "message": "No manifests found to validate", + "total_manifests": 0, + "passed": 0, + "failed": 0, + "results": [] + } + + results = [] + passed = 0 + failed = 0 + + for path, manifest_type in manifests: + logger.info(f"Validating {manifest_type}: {path}") + result = validate_manifest_policies(path, manifest_type) + results.append(result) + + if result.get("success"): + passed += 1 + else: + failed += 1 + + overall_success = failed == 0 + + return { + "success": overall_success, + "mode": "batch", + "message": f"Validated {len(manifests)} manifest(s): {passed} passed, {failed} failed", + "total_manifests": len(manifests), + "passed": passed, + "failed": failed, + "results": results + } + + +def main(): + """Main CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Enforce policy rules for skill and agent manifests" + ) + parser.add_argument( + "manifest_path", + nargs="?", + help="Path to manifest file to validate (omit for batch mode)" + ) + parser.add_argument( + "--batch", + action="store_true", + help="Validate all manifests in skills/ and agents/ directories" + ) + parser.add_argument( + "--strict", + action="store_true", + help="Treat warnings as errors" + ) + + args = parser.parse_args() + + try: + # Batch mode + if args.batch or not args.manifest_path: + logger.info("Running in batch mode") + result = validate_batch(strict=args.strict) + else: + # Single file mode + logger.info(f"Validating single manifest: {args.manifest_path}") + result = validate_manifest_policies(args.manifest_path) + + # Output JSON result + print(json.dumps(result, indent=2)) + + # Exit with appropriate code + sys.exit(0 if result.get("success") else 1) + + except Exception as e: + logger.error(f"Unexpected error: {e}") + error_result = { + "success": False, + "error": str(e), + "message": "Unexpected error during policy enforcement" + } + print(json.dumps(error_result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/policy.enforce/skill.yaml b/skills/policy.enforce/skill.yaml new file mode 100644 index 0000000..34e568e --- /dev/null +++ b/skills/policy.enforce/skill.yaml @@ -0,0 +1,78 @@ +name: policy.enforce +version: 0.1.0 +description: > + Enforces policy rules for skill and agent manifests including naming conventions, + semantic versioning, permissions validation, and status lifecycle checks. Supports + both single-file validation and batch mode for scanning all manifests in skills/ + and agents/ directories. + +inputs: + - name: manifest_path + type: string + required: false + description: Path to a single skill.yaml or agent.yaml manifest file to validate + + - name: batch + type: boolean + required: false + default: false + description: Enable batch mode to scan all manifests in skills/ and agents/ directories + + - name: strict + type: boolean + required: false + default: false + description: Enable strict mode where warnings become errors + +outputs: + - name: validation_report + type: object + description: Detailed validation results including violations, warnings, and success status + + - name: violations + type: array + description: List of policy violations found in the manifest(s) + + - name: success + type: boolean + description: Whether the manifest(s) passed all policy checks + +dependencies: + - context.schema + +status: active + +entrypoints: + - command: /policy/enforce + handler: policy_enforce.py + runtime: python + description: > + Validate skill or agent manifest against policy rules for naming conventions, + semantic versioning, permissions, and status values. + parameters: + - name: manifest_path + type: string + required: false + description: Path to the manifest file to validate (omit for batch mode) + + - name: --batch + type: boolean + required: false + default: false + description: Enable batch mode to validate all manifests + + - name: --strict + type: boolean + required: false + default: false + description: Treat warnings as errors + permissions: + - filesystem + - read + +tags: + - governance + - policy + - validation + - naming + - versioning diff --git a/skills/registry.diff/SKILL.md b/skills/registry.diff/SKILL.md new file mode 100644 index 0000000..1d9635b --- /dev/null +++ b/skills/registry.diff/SKILL.md @@ -0,0 +1,348 @@ +# registry.diff + +Compare current and previous versions of skills/agents and report differences. + +## Overview + +The `registry.diff` skill analyzes changes between a current manifest file (skill.yaml or agent.yaml) and its existing registry entry. It detects various types of changes, determines the required action, and provides suggestions for proper version management and breaking change prevention. + +## Purpose + +- **Version Control**: Ensure proper semantic versioning when updating skills/agents +- **Breaking Change Detection**: Identify changes that break backward compatibility +- **Permission Auditing**: Track permission changes and flag unauthorized modifications +- **Workflow Integration**: Enable automated validation in CI/CD pipelines + +## Usage + +```bash +# Compare a skill manifest against the registry +./skills/registry.diff/registry_diff.py path/to/skill.yaml + +# Compare an agent manifest +./skills/registry.diff/registry_diff.py path/to/agent.yaml + +# Using with betty command (if configured) +betty registry.diff +``` + +## Input + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `manifest_path` | string | Yes | Path to skill.yaml or agent.yaml file to analyze | + +## Output + +The skill returns a JSON response with the following structure: + +```json +{ + "ok": true, + "status": "success", + "errors": [], + "path": "path/to/manifest.yaml", + "details": { + "manifest_path": "path/to/manifest.yaml", + "manifest_type": "skill", + "diff_type": "version_bump", + "required_action": "register", + "suggestions": [ + "Version upgraded: 0.1.0 -> 0.2.0", + "Clean version bump with no breaking changes" + ], + "details": { + "name": "example.skill", + "current_version": "0.2.0", + "previous_version": "0.1.0", + "version_comparison": "upgrade", + "permission_changed": false, + "added_permissions": [], + "removed_permissions": [], + "removed_fields": [], + "status_changed": false, + "current_status": "active", + "previous_status": "active" + }, + "timestamp": "2025-10-23T12:00:00+00:00" + } +} +``` + +### Diff Types + +| Type | Description | +|------|-------------| +| `new` | Entry does not exist in registry (first-time registration) | +| `version_bump` | Version was properly incremented | +| `version_downgrade` | Version was decreased (breaking change) | +| `permission_change` | Permissions were added or removed | +| `breaking_change` | Breaking changes detected (removed fields, etc.) | +| `status_change` | Status field changed (active, deprecated, etc.) | +| `needs_version_bump` | Changes detected without version increment | +| `no_change` | No significant changes detected | + +### Required Actions + +| Action | Description | Exit Code | +|--------|-------------|-----------| +| `register` | Changes are acceptable, proceed with registration | 0 | +| `review` | Manual review recommended before registration | 0 | +| `reject` | Breaking/unauthorized changes, registration blocked | 1 | +| `skip` | No changes detected, no action needed | 0 | + +## Examples + +### Example 1: New Skill Registration + +```bash +$ ./skills/registry.diff/registry_diff.py skills/new.skill/skill.yaml +``` + +Output: +```json +{ + "ok": true, + "status": "success", + "details": { + "diff_type": "new", + "required_action": "register", + "suggestions": [ + "New skill 'new.skill' ready for registration" + ] + } +} +``` + +### Example 2: Clean Version Bump + +```bash +$ ./skills/registry.diff/registry_diff.py skills/example.skill/skill.yaml +``` + +Output: +```json +{ + "ok": true, + "status": "success", + "details": { + "diff_type": "version_bump", + "required_action": "register", + "suggestions": [ + "Version upgraded: 0.1.0 -> 0.2.0", + "Clean version bump with no breaking changes" + ] + } +} +``` + +### Example 3: Breaking Change Detected + +```bash +$ ./skills/registry.diff/registry_diff.py skills/example.skill/skill.yaml +``` + +Output: +```json +{ + "ok": false, + "status": "failed", + "errors": [ + "Fields removed without version bump: dependencies", + "Removing fields requires a version bump", + "Suggested version: 0.2.0" + ], + "details": { + "diff_type": "breaking_change", + "required_action": "reject", + "details": { + "removed_fields": ["dependencies"], + "current_version": "0.1.0", + "previous_version": "0.1.0" + } + } +} +``` +Exit code: 1 + +### Example 4: Permission Change + +```bash +$ ./skills/registry.diff/registry_diff.py skills/example.skill/skill.yaml +``` + +Output: +```json +{ + "ok": true, + "status": "success", + "details": { + "diff_type": "permission_change", + "required_action": "review", + "suggestions": [ + "New permissions added: network", + "Review: Ensure new permissions are necessary and documented" + ], + "details": { + "added_permissions": ["network"], + "removed_permissions": [] + } + } +} +``` + +### Example 5: Version Downgrade (Rejected) + +```bash +$ ./skills/registry.diff/registry_diff.py skills/example.skill/skill.yaml +``` + +Output: +```json +{ + "ok": false, + "status": "failed", + "errors": [ + "Version downgrade detected: 0.2.0 -> 0.1.0", + "Version downgrades are not permitted", + "Suggested action: Restore version to at least 0.2.0" + ], + "details": { + "diff_type": "version_downgrade", + "required_action": "reject" + } +} +``` +Exit code: 1 + +## Exit Codes + +- **0**: Success - Changes are acceptable or no changes detected +- **1**: Failure - Breaking or unauthorized changes detected + +## Workflow Integration + +### Pre-Commit Hook + +```yaml +# .git/hooks/pre-commit +#!/bin/bash +changed_files=$(git diff --cached --name-only | grep -E "skill\.yaml|agent\.yaml") + +for file in $changed_files; do + echo "Checking $file..." + ./skills/registry.diff/registry_diff.py "$file" + if [ $? -ne 0 ]; then + echo "❌ Registry diff failed for $file" + exit 1 + fi +done +``` + +### CI/CD Pipeline + +```yaml +# .github/workflows/validate-changes.yml +name: Validate Skill Changes + +on: [pull_request] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Check skill changes + run: | + for file in $(git diff --name-only origin/main | grep -E "skill\.yaml|agent\.yaml"); do + python3 skills/registry.diff/registry_diff.py "$file" + done +``` + +## Change Detection Rules + +### Breaking Changes (Exit 1) + +1. **Version Downgrade**: Current version < Previous version +2. **Removed Fields**: Fields removed without version bump +3. **Removed Permissions**: Permissions removed without version bump + +### Requires Review (Exit 0, but flagged) + +1. **Permission Addition**: New permissions added +2. **Status Change**: Status changed to deprecated/archived +3. **Needs Version Bump**: Changes without version increment + +### Acceptable Changes (Exit 0) + +1. **New Entry**: First-time registration +2. **Clean Version Bump**: Version incremented properly +3. **No Changes**: No significant changes detected + +## Semantic Versioning Guidelines + +The skill follows semantic versioning principles: + +- **Major (X.0.0)**: Breaking changes, removed functionality +- **Minor (0.X.0)**: New features, deprecated functionality, significant changes +- **Patch (0.0.X)**: Bug fixes, documentation updates, minor tweaks + +### Suggested Version Bumps + +| Change Type | Suggested Bump | +|-------------|----------------| +| Remove fields | Minor | +| Remove permissions | Minor | +| Add permissions | Patch or Minor | +| Status to deprecated | Minor | +| Bug fixes | Patch | +| New features | Minor | +| Breaking changes | Major | + +## Dependencies + +- Python 3.6+ +- `packaging` library (for version comparison) +- Betty Framework core utilities +- PyYAML + +## Related Skills + +- `registry.update`: Update registry entries +- `skill.define`: Define and validate skill manifests +- `audit.log`: Audit trail for registry changes + +## Notes + +- Automatically detects whether input is a skill or agent manifest +- Compares against appropriate registry (skills.json or agents.json) +- Provides human-readable output to stderr for CLI usage +- Returns structured JSON for programmatic integration +- Thread-safe registry reading using Betty Framework utilities + +## Troubleshooting + +### "Manifest file not found" + +Ensure the path to the manifest file is correct and the file exists. + +### "Empty manifest file" + +The YAML file exists but contains no data. Check for valid YAML content. + +### "Registry file not found" + +The registry hasn't been initialized yet. This is normal for new Betty Framework installations. + +### "Invalid YAML in manifest" + +The manifest file contains syntax errors. Validate with a YAML parser. + +## Future Enhancements + +- [ ] Support for dependency tree validation +- [ ] Integration with semantic version recommendation engine +- [ ] Diff visualization in HTML format +- [ ] Support for comparing arbitrary versions (not just latest) +- [ ] Batch mode for comparing multiple manifests diff --git a/skills/registry.diff/__init__.py b/skills/registry.diff/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/registry.diff/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/registry.diff/registry_diff.py b/skills/registry.diff/registry_diff.py new file mode 100755 index 0000000..bcec741 --- /dev/null +++ b/skills/registry.diff/registry_diff.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 +""" +registry.diff - Compare current and previous versions of skills/agents. + +This skill compares a skill or agent manifest against its registry entry +to detect changes and determine appropriate actions. +""" + +import os +import sys +import json +import yaml +from typing import Dict, Any, Optional, List, Tuple +from datetime import datetime, timezone +from packaging import version as version_parser + + +# Betty framework imports +from betty.config import BASE_DIR, REGISTRY_DIR +from betty.file_utils import safe_read_json +from betty.validation import validate_path +from betty.logging_utils import setup_logger +from betty.errors import RegistryError, format_error_response + +logger = setup_logger(__name__) + +SKILLS_REGISTRY = os.path.join(REGISTRY_DIR, "skills.json") +AGENTS_REGISTRY = os.path.join(REGISTRY_DIR, "agents.json") + + +def build_response( + ok: bool, + path: str, + errors: Optional[List[str]] = None, + details: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: + """Standard response format used across all skills.""" + 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_manifest(path: str) -> Dict[str, Any]: + """Load YAML manifest file with error handling.""" + try: + with open(path) as f: + manifest = yaml.safe_load(f) + if not manifest: + raise RegistryError(f"Empty manifest file: {path}") + return manifest + except FileNotFoundError: + raise RegistryError(f"Manifest file not found: {path}") + except yaml.YAMLError as e: + raise RegistryError(f"Invalid YAML in manifest: {e}") + + +def determine_manifest_type(manifest: Dict[str, Any]) -> str: + """Determine if manifest is a skill or agent based on fields.""" + if "entrypoints" in manifest or "handler" in manifest: + return "skill" + elif "capabilities" in manifest or "reasoning_mode" in manifest: + return "agent" + else: + # Default to skill if unclear + return "skill" + + +def find_registry_entry(name: str, manifest_type: str) -> Optional[Dict[str, Any]]: + """Find existing entry in the appropriate registry.""" + registry_file = SKILLS_REGISTRY if manifest_type == "skill" else AGENTS_REGISTRY + + if not os.path.exists(registry_file): + logger.warning(f"Registry file not found: {registry_file}") + return None + + registry = safe_read_json(registry_file, default={}) + entries_key = "skills" if manifest_type == "skill" else "agents" + entries = registry.get(entries_key, []) + + for entry in entries: + if entry.get("name") == name: + return entry + + return None + + +def compare_versions(current: str, previous: str) -> str: + """ + Compare semantic versions. + + Returns: + "upgrade" if current > previous + "downgrade" if current < previous + "same" if current == previous + """ + try: + curr_ver = version_parser.parse(current) + prev_ver = version_parser.parse(previous) + + if curr_ver > prev_ver: + return "upgrade" + elif curr_ver < prev_ver: + return "downgrade" + else: + return "same" + except Exception as e: + logger.warning(f"Error comparing versions: {e}") + return "unknown" + + +def get_permissions(manifest: Dict[str, Any]) -> set: + """Extract permissions from manifest.""" + permissions = set() + + # For skills, permissions are in entrypoints + if "entrypoints" in manifest: + for ep in manifest["entrypoints"]: + if "permissions" in ep: + permissions.update(ep["permissions"]) + + # For agents, might have permissions at top level + if "permissions" in manifest: + perms = manifest["permissions"] + if isinstance(perms, list): + permissions.update(perms) + + return permissions + + +def detect_removed_fields(current: Dict[str, Any], previous: Dict[str, Any]) -> List[str]: + """Detect fields that were removed from the manifest.""" + removed = [] + + # Check top-level fields + for key in previous: + if key not in current: + removed.append(key) + + # Check nested structures like inputs, outputs + for list_field in ["inputs", "outputs", "dependencies", "capabilities", "skills_available"]: + if list_field in previous and list_field in current: + prev_list = previous[list_field] if isinstance(previous[list_field], list) else [] + curr_list = current[list_field] if isinstance(current[list_field], list) else [] + + # For simple lists (strings), use set comparison + if prev_list and all(isinstance(x, (str, int, float, bool)) for x in prev_list): + prev_items = set(prev_list) + curr_items = set(curr_list) + removed_items = prev_items - curr_items + if removed_items: + removed.append(f"{list_field}: {', '.join(str(x) for x in removed_items)}") + # For complex lists (dicts), compare by key fields like 'name' + elif prev_list and all(isinstance(x, dict) for x in prev_list): + prev_names = {item.get('name', item.get('command', str(item))) for item in prev_list} + curr_names = {item.get('name', item.get('command', str(item))) for item in curr_list} + removed_names = prev_names - curr_names + if removed_names: + removed.append(f"{list_field}: {', '.join(removed_names)}") + + return removed + + +def analyze_diff( + manifest: Dict[str, Any], + registry_entry: Optional[Dict[str, Any]], + manifest_type: str +) -> Tuple[str, str, List[str], Dict[str, Any], List[str], bool]: + """ + Analyze differences between current manifest and registry entry. + + Returns: + (diff_type, required_action, suggestions, details, changed_fields, breaking) + """ + name = manifest.get("name", "unknown") + current_version = manifest.get("version", "0.0.0") + changed_fields: List[str] = [] + breaking = False + + # Case 1: New entry (not in registry) + if registry_entry is None: + return ( + "new", + "register", + [f"New {manifest_type} '{name}' ready for registration"], + { + "name": name, + "version": current_version, + "is_new": True + }, + [], # No changed fields for new entries + False # Not breaking for new entries + ) + + # Extract previous values + previous_version = registry_entry.get("version", "0.0.0") + version_comparison = compare_versions(current_version, previous_version) + + # Track version changes + if version_comparison != "same": + changed_fields.append("version") + + # Detect permission changes + current_perms = get_permissions(manifest) + previous_perms = get_permissions(registry_entry) + added_perms = current_perms - previous_perms + removed_perms = previous_perms - current_perms + permission_changed = bool(added_perms or removed_perms) + + if permission_changed: + changed_fields.append("permissions") + + # Detect removed fields + removed_fields = detect_removed_fields(manifest, registry_entry) + + # Track field removals in changed_fields + for field in removed_fields: + # Extract field name (e.g., "inputs: ..." -> "inputs") + field_name = field.split(":")[0] if ":" in field else field + if field_name not in changed_fields: + changed_fields.append(field_name) + + # Detect status changes + current_status = manifest.get("status", "draft") + previous_status = registry_entry.get("status", "draft") + status_changed = current_status != previous_status + + if status_changed: + changed_fields.append("status") + + # Check for description changes + if manifest.get("description") != registry_entry.get("description"): + changed_fields.append("description") + + # Check for other field changes based on manifest type + if manifest_type == "skill": + # Check inputs + if json.dumps(manifest.get("inputs", []), sort_keys=True) != json.dumps(registry_entry.get("inputs", []), sort_keys=True): + if "inputs" not in changed_fields: + changed_fields.append("inputs") + # Check outputs + if json.dumps(manifest.get("outputs", []), sort_keys=True) != json.dumps(registry_entry.get("outputs", []), sort_keys=True): + if "outputs" not in changed_fields: + changed_fields.append("outputs") + # Check entrypoints + if json.dumps(manifest.get("entrypoints", []), sort_keys=True) != json.dumps(registry_entry.get("entrypoints", []), sort_keys=True): + if "entrypoints" not in changed_fields: + changed_fields.append("entrypoints") + elif manifest_type == "agent": + # Check capabilities + if json.dumps(manifest.get("capabilities", []), sort_keys=True) != json.dumps(registry_entry.get("capabilities", []), sort_keys=True): + if "capabilities" not in changed_fields: + changed_fields.append("capabilities") + # Check skills_available + if json.dumps(manifest.get("skills_available", []), sort_keys=True) != json.dumps(registry_entry.get("skills_available", []), sort_keys=True): + if "skills_available" not in changed_fields: + changed_fields.append("skills_available") + # Check reasoning_mode + if manifest.get("reasoning_mode") != registry_entry.get("reasoning_mode"): + changed_fields.append("reasoning_mode") + + # Check dependencies + if json.dumps(manifest.get("dependencies", []), sort_keys=True) != json.dumps(registry_entry.get("dependencies", []), sort_keys=True): + if "dependencies" not in changed_fields: + changed_fields.append("dependencies") + + # Check tags + if json.dumps(sorted(manifest.get("tags", []))) != json.dumps(sorted(registry_entry.get("tags", []))): + if "tags" not in changed_fields: + changed_fields.append("tags") + + # Build details + details = { + "name": name, + "current_version": current_version, + "previous_version": previous_version, + "version_comparison": version_comparison, + "permission_changed": permission_changed, + "added_permissions": list(added_perms), + "removed_permissions": list(removed_perms), + "removed_fields": removed_fields, + "status_changed": status_changed, + "current_status": current_status, + "previous_status": previous_status + } + + suggestions = [] + + # Determine diff type and required action + + # Case 2: Version downgrade (breaking change) + if version_comparison == "downgrade": + breaking = True + return ( + "version_downgrade", + "reject", + [ + f"Version downgrade detected: {previous_version} -> {current_version}", + "Version downgrades are not permitted", + f"Suggested action: Restore version to at least {previous_version}" + ], + details, + changed_fields, + breaking + ) + + # Case 3: Removed fields without version bump (breaking change) + if removed_fields and version_comparison == "same": + breaking = True + suggestions.extend([ + f"Fields removed without version bump: {', '.join(removed_fields)}", + "Removing fields requires a version bump", + f"Suggested version: {increment_version(current_version, 'minor')}" + ]) + return ( + "breaking_change", + "reject", + suggestions, + details, + changed_fields, + breaking + ) + + # Case 4: Permission changes + if permission_changed: + if removed_perms and version_comparison == "same": + breaking = True + suggestions.extend([ + f"Permissions removed without version bump: {', '.join(removed_perms)}", + f"Suggested version: {increment_version(current_version, 'minor')}" + ]) + + if added_perms: + suggestions.append(f"New permissions added: {', '.join(added_perms)}") + suggestions.append("Review: Ensure new permissions are necessary and documented") + + return ( + "permission_change", + "review", + suggestions if suggestions else [f"Permission changes detected in {name}"], + details, + changed_fields, + breaking + ) + + # Case 5: Status change to deprecated/archived without version bump + if status_changed and current_status in ["deprecated", "archived"] and version_comparison == "same": + suggestions.extend([ + f"Status changed to '{current_status}' without version bump", + f"Suggested version: {increment_version(current_version, 'minor')}" + ]) + return ( + "status_change", + "review", + suggestions, + details, + changed_fields, + breaking + ) + + # Case 6: Version bump (normal change) + if version_comparison == "upgrade": + suggestions.append(f"Version upgraded: {previous_version} -> {current_version}") + if not permission_changed and not removed_fields: + suggestions.append("Clean version bump with no breaking changes") + return ( + "version_bump", + "register", + suggestions, + details, + changed_fields, + breaking + ) + + # Case 7: No significant changes + if version_comparison == "same" and not permission_changed and not removed_fields and not status_changed: + return ( + "no_change", + "skip", + [f"No significant changes detected in {name}"], + details, + changed_fields, + breaking + ) + + # Case 8: Changes without version bump (needs review) + suggestions.extend([ + "Changes detected without version bump", + f"Current version: {current_version}", + f"Suggested version: {increment_version(current_version, 'patch')}" + ]) + return ( + "needs_version_bump", + "review", + suggestions, + details, + changed_fields, + breaking + ) + + +def increment_version(version_str: str, bump_type: str = "patch") -> str: + """Increment semantic version.""" + try: + ver = version_parser.parse(version_str) + major, minor, patch = ver.major, ver.minor, ver.micro + + if bump_type == "major": + major += 1 + minor = 0 + patch = 0 + elif bump_type == "minor": + minor += 1 + patch = 0 + else: # patch + patch += 1 + + return f"{major}.{minor}.{patch}" + except Exception: + return version_str + + +def diff_manifest(manifest_path: str) -> Dict[str, Any]: + """ + Compare manifest against registry entry. + + Args: + manifest_path: Path to skill.yaml or agent.yaml + + Returns: + Dictionary with diff analysis + """ + # Validate input + validate_path(manifest_path, must_exist=True) + + # Load manifest + logger.info(f"Loading manifest: {manifest_path}") + manifest = load_manifest(manifest_path) + + # Determine type + manifest_type = determine_manifest_type(manifest) + logger.info(f"Detected manifest type: {manifest_type}") + + # Get name + name = manifest.get("name") + if not name: + raise RegistryError("Manifest missing required 'name' field") + + # Find registry entry + registry_entry = find_registry_entry(name, manifest_type) + + # Analyze differences + diff_type, required_action, suggestions, details, changed_fields, breaking = analyze_diff( + manifest, registry_entry, manifest_type + ) + + # Build result + result = { + "manifest_path": manifest_path, + "manifest_type": manifest_type, + "diff_type": diff_type, + "required_action": required_action, + "changed_fields": changed_fields, + "breaking": breaking, + "suggestions": suggestions, + "details": details, + "timestamp": datetime.now(timezone.utc).isoformat() + } + + logger.info(f"Diff analysis complete: {diff_type} -> {required_action} (breaking: {breaking}, changed: {changed_fields})") + + return result + + +def main(): + """Main CLI entry point.""" + if len(sys.argv) < 2: + message = "Usage: registry_diff.py " + response = build_response( + False, + path="", + errors=[message], + details={ + "error": { + "error": "UsageError", + "message": message, + "details": { + "usage": "registry_diff.py " + } + } + }, + ) + print(json.dumps(response, indent=2)) + sys.exit(1) + + try: + manifest_path = sys.argv[1] + result = diff_manifest(manifest_path) + + # Determine exit code based on required action + should_reject = result["required_action"] == "reject" + exit_code = 1 if should_reject else 0 + + response = build_response( + ok=not should_reject, + path=manifest_path, + errors=result["suggestions"] if should_reject else [], + details=result + ) + + # Print formatted output + print(json.dumps(response, indent=2)) + + # Also print human-readable summary + print("\n" + "="*60, file=sys.stderr) + print(f"Registry Diff Analysis", file=sys.stderr) + print("="*60, file=sys.stderr) + print(f"Manifest: {manifest_path}", file=sys.stderr) + print(f"Type: {result['manifest_type']}", file=sys.stderr) + print(f"Diff Type: {result['diff_type']}", file=sys.stderr) + print(f"Required Action: {result['required_action']}", file=sys.stderr) + print(f"Breaking: {result['breaking']}", file=sys.stderr) + if result['changed_fields']: + print(f"Changed Fields: {', '.join(result['changed_fields'])}", file=sys.stderr) + print("\nSuggestions:", file=sys.stderr) + for suggestion in result["suggestions"]: + print(f" • {suggestion}", file=sys.stderr) + print("="*60 + "\n", file=sys.stderr) + + sys.exit(exit_code) + + except RegistryError as e: + logger.error(str(e)) + error_info = format_error_response(e) + response = build_response( + False, + path=manifest_path if len(sys.argv) > 1 else "", + 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}", exc_info=True) + error_info = format_error_response(e, include_traceback=True) + response = build_response( + False, + path=manifest_path if len(sys.argv) > 1 else "", + errors=[error_info.get("message", str(e))], + details={"error": error_info}, + ) + print(json.dumps(response, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/registry.diff/skill.yaml b/skills/registry.diff/skill.yaml new file mode 100644 index 0000000..9a737c5 --- /dev/null +++ b/skills/registry.diff/skill.yaml @@ -0,0 +1,110 @@ +name: registry.diff +version: 0.2.0 +description: > + Compare current and previous versions of skills/agents and report differences. + Detects changes, determines required actions, and provides suggestions for + version management and breaking change prevention. Enhanced with changed_fields + array and breaking flag for easier consumption. + +# Input parameters +inputs: + - name: manifest_path + type: string + required: true + description: Path to the skill.yaml or agent.yaml manifest file to compare + +# Output files/data +outputs: + - name: diff_result + type: object + description: > + Detailed diff analysis including diff_type, required_action, suggestions, + and comparison details + +# Skills this depends on +dependencies: [] + +# Lifecycle status +status: active + +# Entrypoints define how the skill is invoked +entrypoints: + - command: /registry/diff + handler: registry_diff.py + runtime: python + description: > + Compare a manifest against its registry entry to detect changes and + determine appropriate actions. + parameters: + - name: manifest_path + type: string + required: true + description: > + Path to the skill.yaml or agent.yaml manifest file to analyze. + Can be absolute or relative path. + permissions: + - filesystem + - read + +# Return values +returns: + diff_type: + type: string + enum: + - new + - version_bump + - version_downgrade + - permission_change + - breaking_change + - status_change + - needs_version_bump + - no_change + description: Classification of the detected changes + + required_action: + type: string + enum: + - register + - review + - reject + - skip + description: Recommended action based on the analysis + + changed_fields: + type: array + items: + type: string + description: > + List of field names that changed between the manifest and registry entry + (e.g., version, permissions, status, description, inputs, outputs, etc.) + + breaking: + type: boolean + description: > + Indicates whether the changes are breaking (e.g., version downgrade, + removed fields without version bump, removed permissions without version bump) + + suggestions: + type: array + items: + type: string + description: > + List of suggestions for addressing the detected changes, + including version bump recommendations + +# Exit codes +exit_codes: + 0: > + Success - changes are acceptable (new entry, clean version bump, + or no significant changes) + 1: > + Failure - breaking or unauthorized changes detected (version downgrade, + removed fields without version bump, unauthorized permission changes) + +# Tags for categorization +tags: + - registry + - validation + - version-control + - diff + - comparison diff --git a/skills/registry.query/SKILL.md b/skills/registry.query/SKILL.md new file mode 100644 index 0000000..6482afa --- /dev/null +++ b/skills/registry.query/SKILL.md @@ -0,0 +1,431 @@ +# registry.query + +**Version:** 0.1.0 +**Status:** Active +**Tags:** registry, search, query, discovery, metadata, cli + +## Overview + +The `registry.query` skill enables programmatic searching of Betty registries (skills, agents, and commands) with flexible filtering capabilities. It's designed for dynamic discovery, workflow automation, and CLI autocompletion. + +## Features + +- **Multi-Registry Support**: Query skills, agents, commands, or hooks registries +- **Flexible Filtering**: Filter by name, version, status, tags, domain, and capability +- **Fuzzy Matching**: Optional fuzzy search for name and capability fields +- **Result Limiting**: Control the number of results returned +- **Rich Metadata**: Returns key metadata for each matching entry +- **Multiple Output Formats**: JSON, table, or compact format for different use cases +- **Table Formatting**: Aligned column display for easy CLI viewing + +## Usage + +### Command Line + +```bash +# List all skills (compact format, default) +python3 skills/registry.query/registry_query.py skills + +# Find skills with 'api' tag in table format +python3 skills/registry.query/registry_query.py skills --tag api --format table + +# Find agents with 'design' capability +python3 skills/registry.query/registry_query.py agents --capability design + +# Query hooks registry +python3 skills/registry.query/registry_query.py hooks --status active --format table + +# Find active skills with name containing 'validate' +python3 skills/registry.query/registry_query.py skills --name validate --status active + +# Fuzzy search for commands +python3 skills/registry.query/registry_query.py commands --name test --fuzzy + +# Limit results to top 5 +python3 skills/registry.query/registry_query.py skills --tag api --limit 5 + +# Get full JSON output +python3 skills/registry.query/registry_query.py skills --tag validation --format json +``` + +### Programmatic Use + +```python +from skills.registry.query.registry_query import query_registry + +# Query skills with API tag +result = query_registry( + registry="skills", + tag="api", + status="active" +) + +if result["ok"]: + matching_entries = result["details"]["results"] + for entry in matching_entries: + print(f"{entry['name']}: {entry['description']}") +``` + +### Betty CLI + +```bash +# Via Betty CLI (when registered) +betty registry query skills --tag api +betty registry query agents --capability "API design" +``` + +## Parameters + +### Required + +- **`registry`** (string): Registry to query + - Valid values: `skills`, `agents`, `commands`, `hooks` + +### Optional Filters + +- **`name`** (string): Filter by name (substring match, case-insensitive) +- **`version`** (string): Filter by exact version match +- **`status`** (string): Filter by status (e.g., `active`, `draft`, `deprecated`, `archived`) +- **`tag`** (string): Filter by single tag +- **`tags`** (array): Filter by multiple tags (matches any) +- **`capability`** (string): Filter by capability (agents only, substring match) +- **`domain`** (string): Filter by domain (alias for tag filter) +- **`fuzzy`** (boolean): Enable fuzzy matching for name and capability +- **`limit`** (integer): Maximum number of results to return +- **`format`** (string): Output format (`json`, `table`, `compact`) + - `json`: Full JSON response with all metadata + - `table`: Aligned column table for easy reading + - `compact`: Detailed list format (default) + +## Output Format + +### Success Response + +```json +{ + "ok": true, + "status": "success", + "errors": [], + "timestamp": "2025-10-23T10:30:00.000000Z", + "details": { + "registry": "skills", + "query": { + "name": "api", + "version": null, + "status": "active", + "tags": ["validation"], + "capability": null, + "domain": null, + "fuzzy": false, + "limit": null + }, + "total_entries": 21, + "matching_entries": 3, + "results": [ + { + "name": "api.validate", + "version": "0.1.0", + "description": "Validates OpenAPI or AsyncAPI specifications...", + "status": "active", + "tags": ["api", "validation", "openapi", "asyncapi"], + "dependencies": ["context.schema"], + "entrypoints": [ + { + "command": "/api/validate", + "runtime": "python", + "description": "Validate API specification files" + } + ], + "inputs": [...], + "outputs": [...] + } + ] + } +} +``` + +### Error Response + +```json +{ + "ok": false, + "status": "failed", + "errors": ["Invalid registry: invalid_type"], + "timestamp": "2025-10-23T10:30:00.000000Z" +} +``` + +## Metadata Fields by Registry Type + +### Skills + +- `name`, `version`, `description`, `status`, `tags` +- `dependencies`: List of required skills +- `entrypoints`: Available commands and handlers +- `inputs`: Expected input parameters +- `outputs`: Generated outputs + +### Agents + +- `name`, `version`, `description`, `status`, `tags` +- `capabilities`: List of agent capabilities +- `skills_available`: Skills the agent can invoke +- `reasoning_mode`: `oneshot` or `iterative` +- `context_requirements`: Required context fields + +### Commands + +- `name`, `version`, `description`, `status`, `tags` +- `execution`: Execution configuration (type, target) +- `parameters`: Command parameters + +### Hooks + +- `name`, `version`, `description`, `status`, `tags` +- `event`: Hook event trigger (e.g., on_file_edit, on_commit) +- `command`: Command to execute +- `enabled`: Whether the hook is enabled + +## Use Cases + +### 1. Dynamic Discovery + +Find skills related to a specific domain: + +```bash +python3 skills/registry.query/registry_query.py skills --domain api +``` + +### 2. Workflow Automation + +Programmatically find and invoke skills: + +```python +# Find validation skills +result = query_registry(registry="skills", tag="validation", status="active") + +for skill in result["details"]["results"]: + print(f"Found validation skill: {skill['name']}") + # Invoke skill programmatically +``` + +### 3. CLI Autocompletion + +Generate autocompletion data: + +```python +# Get all active skill names for tab completion +result = query_registry(registry="skills", status="active") +skill_names = [s["name"] for s in result["details"]["results"]] +``` + +### 4. Dependency Resolution + +Find skills with specific dependencies: + +```python +result = query_registry(registry="skills", status="active") +for skill in result["details"]["results"]: + if "context.schema" in skill.get("dependencies", []): + print(f"{skill['name']} depends on context.schema") +``` + +### 5. Capability Search + +Find agents by capability: + +```bash +python3 skills/registry.query/registry_query.py agents --capability "API design" +``` + +### 6. Hooks Management + +Query and monitor hooks: + +```bash +# List all hooks in table format +python3 skills/registry.query/registry_query.py hooks --format table + +# Find hooks by event type +python3 skills/registry.query/registry_query.py hooks --tag commit + +# Find enabled hooks +python3 skills/registry.query/registry_query.py hooks --status active +``` + +### 7. Status Monitoring + +Find deprecated or draft entries: + +```bash +python3 skills/registry.query/registry_query.py skills --status deprecated +python3 skills/registry.query/registry_query.py skills --status draft +``` + +## Future Extensions + +The skill is designed with these future enhancements in mind: + +1. **Advanced Fuzzy Matching**: Implement more sophisticated fuzzy matching algorithms (e.g., Levenshtein distance) +2. **Full-Text Search**: Search within descriptions and documentation +3. **Dependency Graph**: Query dependency relationships between skills +4. **Version Ranges**: Support semantic version range queries (e.g., `>=1.0.0,<2.0.0`) +5. **Sorting Options**: Sort results by name, version, or relevance +6. **Regular Expression Support**: Use regex patterns for advanced filtering +7. **Marketplace Integration**: Query marketplace catalogs with certification status +8. **Performance Caching**: Cache registry data for faster repeated queries + +## Examples + +### Example 1: Find all API-related skills in table format + +```bash +$ python3 skills/registry.query/registry_query.py skills --tag api --format table + +================================================================================ +REGISTRY QUERY: SKILLS +================================================================================ + +Total entries: 21 +Matching entries: 5 + ++-------------------+---------+--------+---------------------------+-------------------------+ +| Name | Version | Status | Tags | Commands | ++-------------------+---------+--------+---------------------------+-------------------------+ +| api.define | 0.1.0 | active | api, openapi, asyncapi | /api/define | +| api.validate | 0.1.0 | active | api, validation, openapi | /api/validate | +| api.compatibility | 0.1.0 | draft | api, compatibility | /api/compatibility | +| api.generate | 0.1.0 | active | api, codegen | /api/generate | +| api.test | 0.1.0 | draft | api, testing | /api/test | ++-------------------+---------+--------+---------------------------+-------------------------+ + +================================================================================ +``` + +### Example 2: Find all API-related skills in compact format + +```bash +$ python3 skills/registry.query/registry_query.py skills --tag api + +================================================================================ +REGISTRY QUERY: SKILLS +================================================================================ + +Total entries: 21 +Matching entries: 5 + +-------------------------------------------------------------------------------- +RESULTS: +-------------------------------------------------------------------------------- + +1. api.define (v0.1.0) + Status: active + Description: Generates OpenAPI 3.1 or AsyncAPI 2.6 specifications from natural language... + Tags: api, openapi, asyncapi, scaffolding + Commands: /api/define + +2. api.validate (v0.1.0) + Status: active + Description: Validates OpenAPI or AsyncAPI specifications against their respective schemas... + Tags: api, validation, openapi, asyncapi + Commands: /api/validate + +... +``` + +### Example 3: Query hooks registry + +```bash +$ python3 skills/registry.query/registry_query.py hooks --format table + +================================================================================ +REGISTRY QUERY: HOOKS +================================================================================ + +Total entries: 3 +Matching entries: 3 + ++------------------+---------+--------+------------------+----------------------------------------+---------+ +| Name | Version | Status | Event | Command | Enabled | ++------------------+---------+--------+------------------+----------------------------------------+---------+ +| pre-commit-lint | 0.1.0 | active | on_commit | python3 hooks/lint.py | True | +| auto-test | 0.1.0 | active | on_file_save | pytest tests/ | True | +| telemetry-log | 0.1.0 | active | on_workflow_end | python3 hooks/telemetry.py --capture | True | ++------------------+---------+--------+------------------+----------------------------------------+---------+ + +================================================================================ +``` + +### Example 4: Find agents that can design APIs + +```bash +$ python3 skills/registry.query/registry_query.py agents --capability design + +================================================================================ +REGISTRY QUERY: AGENTS +================================================================================ + +Total entries: 2 +Matching entries: 1 + +-------------------------------------------------------------------------------- +RESULTS: +-------------------------------------------------------------------------------- + +1. api.designer (v0.1.0) + Status: draft + Description: Design RESTful APIs following best practices and guidelines... + Tags: api, design, openapi + Capabilities: 7 capabilities + Reasoning: iterative +``` + +### Example 5: Fuzzy search with limit + +```bash +$ python3 skills/registry.query/registry_query.py skills --name vld --fuzzy --limit 3 + +# Finds: api.validate, context.validate, etc. +``` + +## Error Handling + +The skill handles various error conditions: + +- **Invalid registry type**: Returns error with valid options +- **Missing registry file**: Returns empty results with warning +- **Invalid JSON**: Returns error with details +- **Invalid filter combinations**: Logs warnings and proceeds with valid filters + +## Performance Considerations + +- Registry files are loaded once per query +- Filtering is performed in memory (O(n) complexity) +- For large registries, use `--limit` to control result size +- Consider caching registry data for repeated queries in production + +## Dependencies + +- **None**: This skill has no dependencies on other Betty skills +- **Python Standard Library**: Uses `json`, `re`, `pathlib` +- **Betty Framework**: Requires `betty.config`, `betty.logging_utils`, `betty.errors` + +## Permissions + +- **`filesystem:read`**: Required to read registry JSON files + +## Contributing + +To extend this skill: + +1. Add new filter types in `filter_entries()` +2. Enhance fuzzy matching in `matches_pattern()` +3. Add new metadata extractors in `extract_key_metadata()` +4. Update tests and documentation + +## See Also + +- **skill.define**: Define new skills +- **agent.define**: Define new agents +- **plugin.sync**: Sync registry files +- **marketplace**: Betty marketplace catalog diff --git a/skills/registry.query/__init__.py b/skills/registry.query/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/registry.query/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/registry.query/registry_query.py b/skills/registry.query/registry_query.py new file mode 100755 index 0000000..c75ea27 --- /dev/null +++ b/skills/registry.query/registry_query.py @@ -0,0 +1,735 @@ +#!/usr/bin/env python3 +""" +registry_query.py - Implementation of the registry.query Skill + +Search Betty registries programmatically by filtering skills, agents, and commands. +Supports filtering by tags, domain, status, name, version, and capability. +""" + +import os +import sys +import json +import re +from typing import Dict, Any, List, Optional, Set +from datetime import datetime, timezone +from pathlib import Path + + +from betty.config import ( + REGISTRY_FILE, + AGENTS_REGISTRY_FILE, + COMMANDS_REGISTRY_FILE, + HOOKS_REGISTRY_FILE, + BASE_DIR +) +from betty.logging_utils import setup_logger +from betty.errors import BettyError + +logger = setup_logger(__name__) + + +def build_response( + ok: bool, + errors: Optional[List[str]] = None, + details: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: + """ + Build standardized response. + + Args: + ok: Whether the operation was successful + errors: List of error messages + details: Additional details to include + + Returns: + Standardized response dictionary + """ + response: Dict[str, Any] = { + "ok": ok, + "status": "success" if ok else "failed", + "errors": errors or [], + "timestamp": datetime.now(timezone.utc).isoformat() + } + if details is not None: + response["details"] = details + return response + + +def load_registry(registry_type: str) -> Dict[str, Any]: + """ + Load a registry file. + + Args: + registry_type: Type of registry ('skills', 'agents', 'commands') + + Returns: + Registry data dictionary + + Raises: + BettyError: If registry cannot be loaded + """ + registry_paths = { + 'skills': REGISTRY_FILE, + 'agents': AGENTS_REGISTRY_FILE, + 'commands': COMMANDS_REGISTRY_FILE, + 'hooks': HOOKS_REGISTRY_FILE + } + + if registry_type not in registry_paths: + raise BettyError( + f"Invalid registry type: {registry_type}", + details={ + "valid_types": list(registry_paths.keys()), + "provided": registry_type + } + ) + + registry_path = registry_paths[registry_type] + + if not os.path.exists(registry_path): + logger.warning(f"Registry not found: {registry_path}") + return { + "version": "1.0.0", + "generated_at": datetime.now(timezone.utc).isoformat(), + registry_type: [] + } + + try: + with open(registry_path) as f: + data = json.load(f) + logger.debug(f"Loaded {registry_type} registry: {len(data.get(registry_type, []))} entries") + return data + except json.JSONDecodeError as e: + raise BettyError(f"Invalid JSON in {registry_type} registry: {e}") + except Exception as e: + raise BettyError(f"Failed to load {registry_type} registry: {e}") + + +def normalize_filter_value(value: Any) -> str: + """ + Normalize a filter value for case-insensitive comparison. + + Args: + value: Value to normalize + + Returns: + Normalized string value + """ + if value is None: + return "" + return str(value).lower().strip() + + +def matches_pattern(text: str, pattern: str, fuzzy: bool = False) -> bool: + """ + Check if text matches a pattern. + + Args: + text: Text to match + pattern: Pattern to match against + fuzzy: Whether to use fuzzy matching + + Returns: + True if text matches pattern + """ + text = normalize_filter_value(text) + pattern = normalize_filter_value(pattern) + + if not pattern: + return True + + if fuzzy: + # Fuzzy match: all characters of pattern appear in order in text + pattern_idx = 0 + for char in text: + if pattern_idx < len(pattern) and char == pattern[pattern_idx]: + pattern_idx += 1 + return pattern_idx == len(pattern) + else: + # Exact substring match + return pattern in text + + +def matches_tags(entry_tags: List[str], filter_tags: List[str]) -> bool: + """ + Check if entry tags match any of the filter tags. + + Args: + entry_tags: Tags from the entry + filter_tags: Tags to filter by + + Returns: + True if any filter tag is in entry tags + """ + if not filter_tags: + return True + + if not entry_tags: + return False + + entry_tags_normalized = [normalize_filter_value(tag) for tag in entry_tags] + filter_tags_normalized = [normalize_filter_value(tag) for tag in filter_tags] + + return any(filter_tag in entry_tags_normalized for filter_tag in filter_tags_normalized) + + +def matches_capabilities(entry_capabilities: List[str], filter_capabilities: List[str], fuzzy: bool = False) -> bool: + """ + Check if entry capabilities match any of the filter capabilities. + + Args: + entry_capabilities: Capabilities from the entry + filter_capabilities: Capabilities to filter by + fuzzy: Whether to use fuzzy matching + + Returns: + True if any capability matches + """ + if not filter_capabilities: + return True + + if not entry_capabilities: + return False + + for filter_cap in filter_capabilities: + for entry_cap in entry_capabilities: + if matches_pattern(entry_cap, filter_cap, fuzzy): + return True + + return False + + +def extract_key_metadata(entry: Dict[str, Any], registry_type: str) -> Dict[str, Any]: + """ + Extract key metadata from an entry based on registry type. + + Args: + entry: Registry entry + registry_type: Type of registry + + Returns: + Dictionary with key metadata + """ + metadata = { + "name": entry.get("name"), + "version": entry.get("version"), + "description": entry.get("description"), + "status": entry.get("status"), + "tags": entry.get("tags", []) + } + + # Add registry-specific metadata + if registry_type == "skills": + # Handle inputs - can be strings or objects + inputs = entry.get("inputs", []) + formatted_inputs = [] + for inp in inputs: + if isinstance(inp, str): + formatted_inputs.append({"name": inp, "type": "string", "required": False}) + elif isinstance(inp, dict): + formatted_inputs.append({ + "name": inp.get("name"), + "type": inp.get("type"), + "required": inp.get("required", False) + }) + + # Handle outputs - can be strings or objects + outputs = entry.get("outputs", []) + formatted_outputs = [] + for out in outputs: + if isinstance(out, str): + formatted_outputs.append({"name": out, "type": "string"}) + elif isinstance(out, dict): + formatted_outputs.append({ + "name": out.get("name"), + "type": out.get("type") + }) + + metadata.update({ + "dependencies": entry.get("dependencies", []), + "entrypoints": [ + { + "command": ep.get("command"), + "runtime": ep.get("runtime"), + "description": ep.get("description") + } + for ep in entry.get("entrypoints", []) + ], + "inputs": formatted_inputs, + "outputs": formatted_outputs + }) + + elif registry_type == "agents": + metadata.update({ + "capabilities": entry.get("capabilities", []), + "skills_available": entry.get("skills_available", []), + "reasoning_mode": entry.get("reasoning_mode"), + "context_requirements": entry.get("context_requirements", {}) + }) + + elif registry_type == "commands": + metadata.update({ + "execution": entry.get("execution", {}), + "parameters": entry.get("parameters", []) + }) + + elif registry_type == "hooks": + metadata.update({ + "event": entry.get("event"), + "command": entry.get("command"), + "enabled": entry.get("enabled", True) + }) + + return metadata + + +def filter_entries( + entries: List[Dict[str, Any]], + registry_type: str, + name: Optional[str] = None, + version: Optional[str] = None, + status: Optional[str] = None, + tags: Optional[List[str]] = None, + capability: Optional[str] = None, + domain: Optional[str] = None, + fuzzy: bool = False +) -> List[Dict[str, Any]]: + """ + Filter entries based on criteria. + + Args: + entries: List of registry entries + registry_type: Type of registry + name: Filter by name (substring match) + version: Filter by version (exact match) + status: Filter by status (exact match) + tags: Filter by tags (any match) + capability: Filter by capability (for agents, substring match) + domain: Filter by domain/tag (alias for tags filter) + fuzzy: Use fuzzy matching for name and capability + + Returns: + List of matching entries with key metadata + """ + results = [] + + # Convert domain to tags if provided + if domain: + tags = tags or [] + if domain not in tags: + tags.append(domain) + + logger.debug(f"Filtering {len(entries)} entries with criteria:") + logger.debug(f" name={name}, version={version}, status={status}") + logger.debug(f" tags={tags}, capability={capability}, fuzzy={fuzzy}") + + for entry in entries: + # Filter by name + if name and not matches_pattern(entry.get("name", ""), name, fuzzy): + continue + + # Filter by version (exact match) + if version and normalize_filter_value(entry.get("version")) != normalize_filter_value(version): + continue + + # Filter by status (exact match) + if status and normalize_filter_value(entry.get("status")) != normalize_filter_value(status): + continue + + # Filter by tags + if tags and not matches_tags(entry.get("tags", []), tags): + continue + + # Filter by capability (agents only) + if capability: + if registry_type == "agents": + capabilities = entry.get("capabilities", []) + if not matches_capabilities(capabilities, [capability], fuzzy): + continue + else: + # For non-agents, skip capability filter + logger.debug(f"Capability filter only applies to agents, skipping for {registry_type}") + + # Entry matches all criteria + metadata = extract_key_metadata(entry, registry_type) + results.append(metadata) + + logger.info(f"Found {len(results)} matching entries") + return results + + +def query_registry( + registry: str, + name: Optional[str] = None, + version: Optional[str] = None, + status: Optional[str] = None, + tag: Optional[str] = None, + tags: Optional[List[str]] = None, + capability: Optional[str] = None, + domain: Optional[str] = None, + fuzzy: bool = False, + limit: Optional[int] = None +) -> Dict[str, Any]: + """ + Query a Betty registry with filters. + + Args: + registry: Registry to query ('skills', 'agents', 'commands') + name: Filter by name + version: Filter by version + status: Filter by status + tag: Single tag filter (convenience parameter) + tags: List of tags to filter by + capability: Filter by capability (agents only) + domain: Domain/tag filter (alias for tags) + fuzzy: Use fuzzy matching + limit: Maximum number of results to return + + Returns: + Query result with matching entries + """ + logger.info(f"Querying {registry} registry") + + # Normalize registry type + registry = registry.lower() + if registry not in ['skills', 'agents', 'commands', 'hooks']: + raise BettyError( + f"Invalid registry: {registry}", + details={ + "valid_registries": ["skills", "agents", "commands", "hooks"], + "provided": registry + } + ) + + # Load registry + registry_data = load_registry(registry) + entries = registry_data.get(registry, []) + + # Merge tag and tags parameters + if tag: + tags = tags or [] + if tag not in tags: + tags.append(tag) + + # Filter entries + results = filter_entries( + entries, + registry, + name=name, + version=version, + status=status, + tags=tags, + capability=capability, + domain=domain, + fuzzy=fuzzy + ) + + # Apply limit + if limit and limit > 0: + results = results[:limit] + + # Build response + return build_response( + ok=True, + details={ + "registry": registry, + "query": { + "name": name, + "version": version, + "status": status, + "tags": tags, + "capability": capability, + "domain": domain, + "fuzzy": fuzzy, + "limit": limit + }, + "total_entries": len(entries), + "matching_entries": len(results), + "results": results + } + ) + + +def format_table(results: List[Dict[str, Any]], registry_type: str) -> str: + """ + Format results as an aligned table. + + Args: + results: List of matching entries + registry_type: Type of registry + + Returns: + Formatted table string + """ + if not results: + return "No matching entries found." + + # Define columns based on registry type + if registry_type == "skills": + columns = ["Name", "Version", "Status", "Tags", "Commands"] + elif registry_type == "agents": + columns = ["Name", "Version", "Status", "Tags", "Reasoning", "Skills"] + elif registry_type == "commands": + columns = ["Name", "Version", "Status", "Tags", "Execution Type"] + elif registry_type == "hooks": + columns = ["Name", "Version", "Status", "Event", "Command", "Enabled"] + else: + columns = ["Name", "Version", "Status", "Description"] + + # Extract data for each column + rows = [] + for entry in results: + if registry_type == "skills": + commands = [ep.get('command', '') for ep in entry.get('entrypoints', [])] + row = [ + entry.get('name', ''), + entry.get('version', ''), + entry.get('status', ''), + ', '.join(entry.get('tags', [])[:3]), # Limit to 3 tags + ', '.join(commands[:2]) # Limit to 2 commands + ] + elif registry_type == "agents": + row = [ + entry.get('name', ''), + entry.get('version', ''), + entry.get('status', ''), + ', '.join(entry.get('tags', [])[:3]), + entry.get('reasoning_mode', ''), + str(len(entry.get('skills_available', []))) + ] + elif registry_type == "commands": + exec_type = entry.get('execution', {}).get('type', '') + row = [ + entry.get('name', ''), + entry.get('version', ''), + entry.get('status', ''), + ', '.join(entry.get('tags', [])[:3]), + exec_type + ] + elif registry_type == "hooks": + row = [ + entry.get('name', ''), + entry.get('version', ''), + entry.get('status', ''), + entry.get('event', ''), + entry.get('command', '')[:40], # Truncate long commands + str(entry.get('enabled', True)) + ] + else: + row = [ + entry.get('name', ''), + entry.get('version', ''), + entry.get('status', ''), + entry.get('description', '')[:50] + ] + rows.append(row) + + # Calculate column widths + col_widths = [len(col) for col in columns] + for row in rows: + for i, cell in enumerate(row): + col_widths[i] = max(col_widths[i], len(str(cell))) + + # Build table + lines = [] + separator = "+" + "+".join("-" * (w + 2) for w in col_widths) + "+" + + # Header + lines.append(separator) + header = "|" + "|".join(f" {col:<{col_widths[i]}} " for i, col in enumerate(columns)) + "|" + lines.append(header) + lines.append(separator) + + # Rows + for row in rows: + row_str = "|" + "|".join(f" {str(cell):<{col_widths[i]}} " for i, cell in enumerate(row)) + "|" + lines.append(row_str) + + lines.append(separator) + + return "\n".join(lines) + + +def main(): + """Main CLI entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Query Betty registries programmatically", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # List all skills (compact format) + registry_query.py skills + + # Find skills with 'api' tag in table format + registry_query.py skills --tag api --format table + + # Find agents with 'design' capability + registry_query.py agents --capability design + + # Find active skills with name containing 'validate' + registry_query.py skills --name validate --status active + + # Query hooks registry + registry_query.py hooks --status active --format table + + # Fuzzy search for commands + registry_query.py commands --name test --fuzzy + + # Limit results with JSON output + registry_query.py skills --tag api --limit 5 --format json + """ + ) + + parser.add_argument( + "registry", + choices=["skills", "agents", "commands", "hooks"], + help="Registry to query" + ) + parser.add_argument( + "--name", + help="Filter by name (substring match)" + ) + parser.add_argument( + "--version", + help="Filter by version (exact match)" + ) + parser.add_argument( + "--status", + help="Filter by status (e.g., active, draft, deprecated)" + ) + parser.add_argument( + "--tag", + help="Filter by single tag" + ) + parser.add_argument( + "--tags", + nargs="+", + help="Filter by multiple tags (any match)" + ) + parser.add_argument( + "--capability", + help="Filter by capability (agents only)" + ) + parser.add_argument( + "--domain", + help="Filter by domain (alias for tag)" + ) + parser.add_argument( + "--fuzzy", + action="store_true", + help="Use fuzzy matching for name and capability" + ) + parser.add_argument( + "--limit", + type=int, + help="Maximum number of results to return" + ) + parser.add_argument( + "--format", + choices=["json", "table", "compact"], + default="compact", + help="Output format (default: compact)" + ) + + args = parser.parse_args() + + try: + result = query_registry( + registry=args.registry, + name=args.name, + version=args.version, + status=args.status, + tag=args.tag, + tags=args.tags, + capability=args.capability, + domain=args.domain, + fuzzy=args.fuzzy, + limit=args.limit + ) + + details = result["details"] + + if args.format == "json": + # Output full JSON + print(json.dumps(result, indent=2)) + elif args.format == "table": + # Table format + print(f"\n{'='*80}") + print(f"REGISTRY QUERY: {details['registry'].upper()}") + print(f"{'='*80}") + print(f"\nTotal entries: {details['total_entries']}") + print(f"Matching entries: {details['matching_entries']}\n") + + if details['results']: + print(format_table(details['results'], details['registry'])) + else: + print("No matching entries found.") + + print(f"\n{'='*80}\n") + else: + # Compact format (original pretty print) + print(f"\n{'='*80}") + print(f"REGISTRY QUERY: {details['registry'].upper()}") + print(f"{'='*80}") + print(f"\nTotal entries: {details['total_entries']}") + print(f"Matching entries: {details['matching_entries']}") + + if details['results']: + print(f"\n{'-'*80}") + print("RESULTS:") + print(f"{'-'*80}\n") + + for i, entry in enumerate(details['results'], 1): + print(f"{i}. {entry['name']} (v{entry['version']})") + print(f" Status: {entry['status']}") + print(f" Description: {entry['description'][:80]}...") + if entry.get('tags'): + print(f" Tags: {', '.join(entry['tags'])}") + + # Registry-specific details + if details['registry'] == 'skills': + if entry.get('entrypoints'): + commands = [ep['command'] for ep in entry['entrypoints']] + print(f" Commands: {', '.join(commands)}") + elif details['registry'] == 'agents': + if entry.get('capabilities'): + print(f" Capabilities: {len(entry['capabilities'])} capabilities") + print(f" Reasoning: {entry.get('reasoning_mode', 'unknown')}") + elif details['registry'] == 'hooks': + print(f" Event: {entry.get('event', 'unknown')}") + print(f" Command: {entry.get('command', 'unknown')}") + print(f" Enabled: {entry.get('enabled', True)}") + + print() + + print(f"{'-'*80}") + else: + print("\nNo matching entries found.") + + print(f"\n{'='*80}\n") + + sys.exit(0 if result['ok'] else 1) + + except BettyError as e: + logger.error(f"Query failed: {e}") + result = build_response( + ok=False, + errors=[str(e)] + ) + print(json.dumps(result, indent=2)) + sys.exit(1) + + except Exception as e: + logger.error(f"Unexpected error: {e}", exc_info=True) + result = build_response( + ok=False, + errors=[f"Unexpected error: {str(e)}"] + ) + print(json.dumps(result, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/registry.query/skill.yaml b/skills/registry.query/skill.yaml new file mode 100644 index 0000000..e028203 --- /dev/null +++ b/skills/registry.query/skill.yaml @@ -0,0 +1,131 @@ +name: registry.query +version: 0.1.0 +description: > + Search Betty registries programmatically by filtering skills, agents, commands, and hooks. + Supports filtering by tags, domain, status, name, version, and capability with optional + fuzzy matching for dynamic discovery and CLI autocompletion. Includes table formatting + for easy viewing in CLI. + +inputs: + - name: registry + type: string + required: true + description: Registry to query (skills, agents, commands, or hooks) + - name: name + type: string + required: false + description: Filter by name (substring match, supports fuzzy matching) + - name: version + type: string + required: false + description: Filter by version (exact match) + - name: status + type: string + required: false + description: Filter by status (e.g., active, draft, deprecated, archived) + - name: tag + type: string + required: false + description: Filter by single tag + - name: tags + type: array + required: false + description: Filter by multiple tags (any match) + - name: capability + type: string + required: false + description: Filter by capability (agents only, substring match) + - name: domain + type: string + required: false + description: Filter by domain (alias for tag filtering) + - name: fuzzy + type: boolean + required: false + description: Enable fuzzy matching for name and capability filters + - name: limit + type: integer + required: false + description: Maximum number of results to return + - name: format + type: string + required: false + description: Output format (json, table, compact) + +outputs: + - name: results + type: array + description: List of matching registry entries with key metadata + - name: query_metadata + type: object + description: Query statistics including total entries and matching count + - name: registry_info + type: object + description: Information about the queried registry + +dependencies: [] + +status: active + +entrypoints: + - command: /registry/query + handler: registry_query.py + runtime: python + description: > + Query Betty registries with flexible filtering options. Returns matching entries + with key metadata for programmatic use or CLI exploration. + parameters: + - name: registry + type: string + required: true + description: Registry to query (skills, agents, commands, or hooks) + - name: name + type: string + required: false + description: Filter by name (substring match) + - name: version + type: string + required: false + description: Filter by version (exact match) + - name: status + type: string + required: false + description: Filter by status + - name: tag + type: string + required: false + description: Filter by single tag + - name: tags + type: array + required: false + description: Filter by multiple tags + - name: capability + type: string + required: false + description: Filter by capability (agents only) + - name: domain + type: string + required: false + description: Filter by domain + - name: fuzzy + type: boolean + required: false + description: Enable fuzzy matching + - name: limit + type: integer + required: false + description: Maximum results to return + - name: format + type: string + required: false + description: Output format (json, table, compact) + permissions: + - filesystem:read + +tags: + - registry + - search + - query + - discovery + - metadata + - cli diff --git a/skills/registry.update/SKILL.md b/skills/registry.update/SKILL.md new file mode 100644 index 0000000..a609c1e --- /dev/null +++ b/skills/registry.update/SKILL.md @@ -0,0 +1,177 @@ +--- +name: Registry Update +description: Updates the Betty Framework Skill Registry when new skills are created or validated. +--- + +# registry.update + +## Purpose + +The `registry.update` skill centralizes all changes to `/registry/skills.json`. +Instead of each skill writing to the registry directly, they call this skill to ensure consistency, policy enforcement, and audit logging. + +## Usage + +### Basic Usage + +```bash +python skills/registry.update/registry_update.py +``` + +### Arguments + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| manifest_path | string | Yes | Path to the skill manifest file (skill.yaml) | + +## Behavior + +1. **Policy Enforcement**: Runs `policy.enforce` skill (if available) to validate the manifest against organizational policies +2. **Load Manifest**: Reads and parses the skill manifest YAML +3. **Update Registry**: Adds or updates the skill entry in `/registry/skills.json` +4. **Thread-Safe**: Uses file locking to ensure safe concurrent updates +5. **Audit Trail**: Records all registry modifications + +## Outputs + +### Success Response + +```json +{ + "ok": true, + "status": "success", + "errors": [], + "path": "skills/api.validate/skill.yaml", + "details": { + "skill_name": "api.validate", + "version": "0.1.0", + "action": "updated", + "registry_file": "/registry/skills.json", + "policy_enforced": true + } +} +``` + +### Failure Response (Policy Violation) + +```json +{ + "ok": false, + "status": "failed", + "errors": [ + "Policy violations detected:", + " - Skill name must follow domain.action pattern", + " - Description must be at least 20 characters" + ], + "path": "skills/bad-skill/skill.yaml" +} +``` + +## Policy Enforcement + +Before updating the registry, this skill runs `policy.enforce` (if available) to validate: + +- **Naming Conventions**: Skills follow `domain.action` pattern +- **Required Fields**: All mandatory fields present and valid +- **Dependencies**: Referenced dependencies exist in registry +- **Version Conflicts**: No version conflicts with existing skills + +If policy enforcement fails, the registry update is **blocked** and errors are returned. + +## Thread Safety + +The skill uses file locking via `safe_update_json` to ensure: +- Multiple concurrent updates don't corrupt the registry +- Atomic read-modify-write operations +- Proper error handling and rollback on failure + +## Integration + +### With skill.define + +`skill.define` automatically calls `registry.update` after validation: + +```bash +# This validates AND updates registry +python skills/skill.define/skill_define.py skills/my.skill/skill.yaml +``` + +### With skill.create + +`skill.create` scaffolds a skill and registers it: + +```bash +python skills/skill.create/skill_create.py my.skill "Does something" +# Internally calls skill.define which calls registry.update +``` + +### Direct Usage + +For manual registry updates: + +```bash +python skills/registry.update/registry_update.py skills/custom.skill/skill.yaml +``` + +## Registry Structure + +The `/registry/skills.json` file has this structure: + +```json +{ + "registry_version": "1.0.0", + "generated_at": "2025-10-23T12:00:00Z", + "skills": [ + { + "name": "api.validate", + "version": "0.1.0", + "description": "Validate OpenAPI specifications", + "inputs": ["spec_path", "guideline_set"], + "outputs": ["validation_report", "valid"], + "dependencies": ["context.schema"], + "status": "active", + "entrypoints": [...], + "tags": ["api", "validation"] + } + ] +} +``` + +## Files Modified + +- **Registry**: `/registry/skills.json` – Updated with skill entry +- **Logs**: Registry updates logged to Betty's logging system + +## Exit Codes + +- **0**: Success (registry updated) +- **1**: Failure (policy violation or update failed) + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "Manifest file not found" | Path incorrect or file doesn't exist | Check the path to skill.yaml | +| "Policy violations detected" | Skill doesn't meet requirements | Fix policy violations listed in errors | +| "Invalid YAML in manifest" | Malformed YAML syntax | Fix YAML syntax errors | + +## Best Practices + +1. **Use via skill.define**: Don't call directly unless needed +2. **Policy Compliance**: Ensure skills pass policy checks before registration +3. **Version Control**: Keep registry changes in git for full history +4. **Atomic Updates**: The skill handles thread safety automatically + +## See Also + +- **skill.define** – Validates manifests before calling registry.update ([skill.define SKILL.md](../skill.define/SKILL.md)) +- **policy.enforce** – Enforces organizational policies (if configured) +- **Betty Architecture** – [Five-Layer Model](../../docs/betty-architecture.md) + +## Status + +**Active** – Production-ready, core infrastructure skill + +## Version History + +- **0.1.0** (Oct 2025) – Initial implementation with policy enforcement and thread-safe updates diff --git a/skills/registry.update/__init__.py b/skills/registry.update/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/registry.update/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/registry.update/registry_update.py b/skills/registry.update/registry_update.py new file mode 100644 index 0000000..a29e410 --- /dev/null +++ b/skills/registry.update/registry_update.py @@ -0,0 +1,701 @@ +#!/usr/bin/env python3 +""" +registry_update.py – Implementation of the registry.update Skill +Adds, updates, or removes entries in the Betty Framework Skill Registry. +""" + +import os +import sys +import json +import yaml +import subprocess +from typing import Dict, Any, Optional, List +from datetime import datetime, timezone +from packaging import version as version_parser +from pydantic import ValidationError as PydanticValidationError + + +from betty.config import BASE_DIR, REGISTRY_FILE, REGISTRY_VERSION, get_skill_handler_path, REGISTRY_DIR +from betty.file_utils import safe_update_json +from betty.validation import validate_path +from betty.logging_utils import setup_logger +from betty.errors import RegistryError, VersionConflictError, format_error_response +from betty.telemetry_capture import capture_execution +from betty.models import SkillManifest +from betty.provenance import compute_hash, get_provenance_logger +from betty.versioning import is_monotonic_increase, parse_version + +logger = setup_logger(__name__) + +AGENTS_REGISTRY_FILE = os.path.join(REGISTRY_DIR, "agents.json") + + +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_manifest(path: str) -> Dict[str, Any]: + """ + Load a skill manifest from YAML file. + + Args: + path: Path to skill manifest file + + Returns: + Parsed manifest dictionary + + Raises: + RegistryError: If manifest cannot be loaded + """ + try: + with open(path) as f: + manifest = yaml.safe_load(f) + return manifest + except FileNotFoundError: + raise RegistryError(f"Manifest file not found: {path}") + except yaml.YAMLError as e: + raise RegistryError(f"Invalid YAML in manifest: {e}") + + +def validate_manifest_schema(manifest: Dict[str, Any]) -> None: + """ + Validate manifest using Pydantic schema. + + Args: + manifest: Manifest data dictionary + + Raises: + RegistryError: If schema validation fails with type "SchemaError" + """ + try: + SkillManifest.model_validate(manifest) + logger.info("Pydantic schema validation passed for manifest") + except PydanticValidationError as exc: + logger.error("Pydantic schema validation failed") + # Convert Pydantic errors to human-readable messages + error_messages = [] + for error in exc.errors(): + field = ".".join(str(loc) for loc in error["loc"]) + message = error["msg"] + error_type = error["type"] + error_messages.append(f"Schema validation error at '{field}': {message} (type: {error_type})") + + error_detail = "\n".join(error_messages) + raise RegistryError( + f"Manifest schema validation failed:\n{error_detail}" + ) from exc + + +def enforce_policy(manifest_path: str) -> Dict[str, Any]: + """ + Run policy enforcement on the manifest before registry update. + + Args: + manifest_path: Path to skill manifest file + + Returns: + Policy enforcement result + + Raises: + RegistryError: If policy enforcement fails or violations are detected + """ + try: + policy_handler = get_skill_handler_path("policy.enforce") + logger.info(f"Running policy enforcement on: {manifest_path}") + + result = subprocess.run( + [sys.executable, policy_handler, manifest_path], + capture_output=True, + text=True, + timeout=30 + ) + + # Try to parse JSON output + policy_result = None + if result.stdout.strip(): + try: + policy_result = json.loads(result.stdout.strip()) + except json.JSONDecodeError: + logger.warning("Failed to parse policy enforcement output as JSON") + + # Check if policy enforcement passed + if result.returncode != 0: + errors = [] + if policy_result and isinstance(policy_result, dict): + errors = policy_result.get("errors", []) + if not errors: + errors = [f"Policy enforcement failed with return code {result.returncode}"] + + error_msg = "Policy violations detected:\n" + "\n".join(f" - {err}" for err in errors) + logger.error(error_msg) + raise RegistryError(error_msg) + + if policy_result and not policy_result.get("ok", False): + errors = policy_result.get("errors", ["Unknown policy violation"]) + error_msg = "Policy violations detected:\n" + "\n".join(f" - {err}" for err in errors) + logger.error(error_msg) + raise RegistryError(error_msg) + + logger.info("✅ Policy enforcement passed") + return policy_result or {} + + except subprocess.TimeoutExpired: + raise RegistryError("Policy enforcement timed out") + except FileNotFoundError: + logger.warning("policy.enforce skill not found, skipping policy enforcement") + return {} + except Exception as e: + if isinstance(e, RegistryError): + raise + logger.error(f"Failed to run policy enforcement: {e}") + raise RegistryError(f"Failed to run policy enforcement: {e}") + + +def increment_version(version_str: str, bump_type: str = "patch") -> str: + """ + Increment semantic version. + + Args: + version_str: Current version string (e.g., "1.2.3") + bump_type: Type of version bump ("major", "minor", or "patch") + + Returns: + Incremented version string + """ + try: + ver = version_parser.parse(version_str) + major, minor, patch = ver.major, ver.minor, ver.micro + + if bump_type == "major": + major += 1 + minor = 0 + patch = 0 + elif bump_type == "minor": + minor += 1 + patch = 0 + else: # patch + patch += 1 + + return f"{major}.{minor}.{patch}" + except Exception as e: + logger.warning(f"Error incrementing version {version_str}: {e}") + return version_str + + +def run_registry_diff(manifest_path: str) -> Optional[Dict[str, Any]]: + """ + Run registry.diff to analyze changes in the manifest. + + Args: + manifest_path: Path to skill manifest file + + Returns: + Diff analysis result or None if diff fails + """ + try: + diff_handler = get_skill_handler_path("registry.diff") + logger.info(f"Running registry.diff on: {manifest_path}") + + result = subprocess.run( + [sys.executable, diff_handler, manifest_path], + capture_output=True, + text=True, + timeout=30 + ) + + # Parse JSON output - registry.diff prints JSON as first output + if result.stdout.strip(): + try: + # Split by newlines and get the first JSON block + lines = result.stdout.strip().split('\n') + json_lines = [] + in_json = False + brace_count = 0 + + for line in lines: + if line.strip().startswith('{'): + in_json = True + if in_json: + json_lines.append(line) + brace_count += line.count('{') - line.count('}') + if brace_count == 0: + break + + json_str = '\n'.join(json_lines) + diff_result = json.loads(json_str) + + if diff_result and "details" in diff_result: + return diff_result["details"] + except json.JSONDecodeError as e: + logger.warning(f"Failed to parse registry.diff output as JSON: {e}") + + return None + + except subprocess.TimeoutExpired: + logger.warning("registry.diff timed out") + return None + except FileNotFoundError: + logger.warning("registry.diff skill not found, skipping diff analysis") + return None + except Exception as e: + logger.warning(f"Failed to run registry.diff: {e}") + return None + + +def determine_version_bump(diff_result: Dict[str, Any]) -> tuple[str, str]: + """ + Determine the type of version bump needed based on diff analysis. + + Rules: + - Field removed → major bump + - Field or permission added → minor bump + - No breaking change → patch bump + + Args: + diff_result: Result from registry.diff + + Returns: + Tuple of (bump_type, reason) + """ + diff_type = diff_result.get("diff_type", "") + breaking = diff_result.get("breaking", False) + changed_fields = diff_result.get("changed_fields", []) + details = diff_result.get("details", {}) + + # Extract specific changes + removed_fields = details.get("removed_fields", []) + removed_perms = details.get("removed_permissions", []) + added_perms = details.get("added_permissions", []) + + # Filter out metadata fields that are added by registry (not user-defined) + # These fields should not trigger version bumps + metadata_fields = ["updated_at", "version_bump_reason"] + removed_fields = [f for f in removed_fields if f not in metadata_fields] + changed_fields = [f for f in changed_fields if f not in metadata_fields] + + reasons = [] + + # Rule 1: Field removed → major bump + if removed_fields: + reasons.append(f"Removed fields: {', '.join(removed_fields)}") + return "major", "; ".join(reasons) + + # Rule 2: Permission removed → major bump (breaking change) + if removed_perms: + reasons.append(f"Removed permissions: {', '.join(removed_perms)}") + return "major", "; ".join(reasons) + + # Rule 3: Field or permission added → minor bump + if added_perms: + reasons.append(f"Added permissions: {', '.join(added_perms)}") + return "minor", "; ".join(reasons) + + # Check for new fields (compare changed_fields) + # Fields that are in changed_fields but not version/description/status + non_trivial_changes = [ + f for f in changed_fields + if f not in ["version", "description", "updated_at", "tags"] + ] + + if non_trivial_changes: + # Check if fields were added (not just modified) + # This would need more sophisticated detection, but for now + # we'll treat new inputs/outputs/capabilities as minor bumps + if any(f in non_trivial_changes for f in ["inputs", "outputs", "capabilities", "skills_available", "entrypoints"]): + reasons.append(f"Modified fields: {', '.join(non_trivial_changes)}") + return "minor", "; ".join(reasons) + + # Rule 4: No breaking change → patch bump + if changed_fields and not breaking: + reasons.append(f"Updated fields: {', '.join(changed_fields)}") + return "patch", "; ".join(reasons) + + # Default to patch for any other changes + if diff_type not in ["new", "no_change"]: + return "patch", "General updates" + + return "patch", "No significant changes" + + +def apply_auto_version(manifest_path: str, manifest: Dict[str, Any]) -> tuple[Dict[str, Any], Optional[str]]: + """ + Apply automatic version bumping to the manifest. + + Args: + manifest_path: Path to manifest file + manifest: Loaded manifest data + + Returns: + Tuple of (updated_manifest, version_bump_reason) or (manifest, None) if no bump + """ + # Run diff analysis + diff_result = run_registry_diff(manifest_path) + + if not diff_result: + logger.info("No diff result available, skipping auto-version") + return manifest, None + + diff_type = diff_result.get("diff_type", "") + + # Skip auto-versioning for new entries or no changes + if diff_type in ["new", "no_change"]: + logger.info(f"Diff type '{diff_type}' does not require auto-versioning") + return manifest, None + + # Skip if version was already bumped + if diff_type == "version_bump": + logger.info("Version already bumped manually, skipping auto-version") + return manifest, None + + # Determine version bump type + bump_type, reason = determine_version_bump(diff_result) + + current_version = manifest.get("version", "0.0.0") + new_version = increment_version(current_version, bump_type) + + logger.info(f"Auto-versioning: {current_version} → {new_version} ({bump_type} bump)") + logger.info(f"Reason: {reason}") + + # Update manifest + updated_manifest = manifest.copy() + updated_manifest["version"] = new_version + updated_manifest["updated_at"] = datetime.now(timezone.utc).isoformat() + + # Save updated manifest back to file + try: + with open(manifest_path, 'w') as f: + yaml.safe_dump(updated_manifest, f, default_flow_style=False, sort_keys=False) + logger.info(f"Updated manifest file with new version: {manifest_path}") + except Exception as e: + logger.warning(f"Failed to write updated manifest: {e}") + + return updated_manifest, reason + + +def enforce_version_constraints(manifest: Dict[str, Any], registry_data: Dict[str, Any]) -> None: + """ + Enforce semantic version constraints on manifest updates. + + Rules: + - Version field is required on all entries + - Cannot overwrite an active version with the same version number + - Version must be monotonically increasing (no downgrades) + + Args: + manifest: Skill manifest to validate + registry_data: Current registry data + + Raises: + RegistryError: If version field is missing + VersionConflictError: If version constraints are violated + """ + skill_name = manifest.get("name") + new_version = manifest.get("version") + + # Rule 1: Require explicit version field + if not new_version: + raise RegistryError( + f"Manifest for '{skill_name}' missing required 'version' field. " + "All registry entries must have an explicit semantic version." + ) + + # Validate version format + try: + parse_version(new_version) + except Exception as e: + raise RegistryError(f"Invalid version format '{new_version}': {e}") + + # Find existing entry in registry + existing_entry = None + for skill in registry_data.get("skills", []): + if skill.get("name") == skill_name: + existing_entry = skill + break + + if existing_entry: + old_version = existing_entry.get("version") + old_status = existing_entry.get("status", "draft") + + if old_version: + # Rule 2: Refuse overwriting an active version with same version + if new_version == old_version and old_status == "active": + raise VersionConflictError( + f"Cannot overwrite active version {old_version} of '{skill_name}'. " + f"Active versions are immutable. Please increment the version number." + ) + + # Rule 3: Enforce monotonic SemVer order (no downgrades) + if not is_monotonic_increase(old_version, new_version): + # Allow same version if status is draft (for iterative development) + if new_version == old_version and old_status == "draft": + logger.info(f"Allowing same version {new_version} for draft skill '{skill_name}'") + else: + raise VersionConflictError( + f"Version downgrade or same version detected for '{skill_name}': " + f"{old_version} -> {new_version}. " + f"Versions must follow monotonic SemVer order (e.g., 0.2.0 < 0.3.0)." + ) + + +def update_registry_data(manifest_path: str, auto_version: bool = False) -> Dict[str, Any]: + """ + Update the registry with a skill manifest. + + Uses file locking to ensure thread-safe updates. + + Args: + manifest_path: Path to skill manifest file + auto_version: Whether to automatically increment version based on changes + + Returns: + Result dictionary with update status + + Raises: + RegistryError: If update fails + VersionConflictError: If version constraints are violated + """ + # Validate path + validate_path(manifest_path, must_exist=True) + + # Load manifest + manifest = load_manifest(manifest_path) + + # Validate manifest schema with Pydantic + validate_manifest_schema(manifest) + + # Enforce policy before updating registry + policy_result = enforce_policy(manifest_path) + + if not manifest.get("name"): + raise RegistryError("Manifest missing required 'name' field") + + skill_name = manifest["name"] + logger.info(f"Updating registry with skill: {skill_name}") + + # Apply auto-versioning if enabled + version_bump_reason = None + if auto_version: + logger.info("Auto-versioning enabled") + manifest, version_bump_reason = apply_auto_version(manifest_path, manifest) + if version_bump_reason: + logger.info(f"Auto-version applied: {manifest.get('version')} - {version_bump_reason}") + + # Capture registry state before update for diff tracking + registry_before = None + try: + if os.path.exists(REGISTRY_FILE): + with open(REGISTRY_FILE, 'r') as f: + registry_before = json.load(f) + except Exception: + pass # Ignore errors reading before state + + # Enforce version constraints before update + # Use registry_before if available, otherwise use default empty structure + registry_for_validation = registry_before if registry_before else { + "registry_version": REGISTRY_VERSION, + "generated_at": datetime.now(timezone.utc).isoformat(), + "skills": [] + } + enforce_version_constraints(manifest, registry_for_validation) + + def update_fn(registry_data): + """Update function for safe_update_json with provenance tracking.""" + # Ensure registry has proper structure + if not registry_data or "skills" not in registry_data: + registry_data = { + "registry_version": REGISTRY_VERSION, + "generated_at": datetime.now(timezone.utc).isoformat(), + "skills": [] + } + + # Remove existing entry if present + registry_data["skills"] = [ + s for s in registry_data["skills"] + if s.get("name") != skill_name + ] + + # Prepare registry entry + registry_entry = manifest.copy() + + # Add version bump metadata if auto-versioned + if version_bump_reason: + registry_entry["version_bump_reason"] = version_bump_reason + + # Ensure updated_at timestamp + if "updated_at" not in registry_entry: + registry_entry["updated_at"] = datetime.now(timezone.utc).isoformat() + + # Add new entry + registry_data["skills"].append(registry_entry) + + # Update timestamp + registry_data["generated_at"] = datetime.now(timezone.utc).isoformat() + + # Compute content hash for provenance tracking + content_hash = compute_hash(registry_data) + registry_data["content_hash"] = content_hash + + # Log to provenance system + try: + provenance = get_provenance_logger() + provenance.log_artifact( + artifact_id="skills.json", + version=registry_data.get("registry_version", "unknown"), + content_hash=content_hash, + artifact_type="registry", + metadata={ + "total_skills": len(registry_data.get("skills", [])), + "updated_skill": skill_name, + "skill_version": manifest.get("version", "unknown"), + } + ) + logger.info(f"Provenance logged: skills.json -> {content_hash[:8]}...") + except Exception as e: + logger.warning(f"Failed to log provenance: {e}") + + return registry_data + + # Default registry structure + default_registry = { + "registry_version": REGISTRY_VERSION, + "generated_at": datetime.now(timezone.utc).isoformat(), + "skills": [] + } + + try: + # Capture telemetry with diff tracking + with capture_execution( + skill_name="registry.update", + inputs={"manifest_path": manifest_path, "skill_name": skill_name}, + caller="cli" + ) as ctx: + # Use safe atomic update with file locking + updated_registry = safe_update_json(REGISTRY_FILE, update_fn, default=default_registry) + + # Calculate diff for telemetry + registry_diff = None + if registry_before: + skills_before = {s.get("name"): s for s in registry_before.get("skills", [])} + skills_after = {s.get("name"): s for s in updated_registry.get("skills", [])} + + # Determine if this was an add, update, or no change + if skill_name not in skills_before: + operation = "add" + elif skills_before.get(skill_name) != skills_after.get(skill_name): + operation = "update" + else: + operation = "no_change" + + registry_diff = { + "operation": operation, + "skill_name": skill_name, + "skills_before": len(skills_before), + "skills_after": len(skills_after), + } + + # Add metadata to telemetry + ctx.set_metadata( + registry_path=REGISTRY_FILE, + total_skills=len(updated_registry["skills"]), + policy_enforced=True, + diff=registry_diff, + ) + + result = { + "status": "success", + "updated": skill_name, + "registry_path": REGISTRY_FILE, + "total_skills": len(updated_registry["skills"]), + "timestamp": datetime.now(timezone.utc).isoformat() + } + + # Add auto-versioning info if applicable + if version_bump_reason: + result["auto_versioned"] = True + result["version"] = manifest.get("version") + result["version_bump_reason"] = version_bump_reason + + logger.info(f"✅ Successfully updated registry for: {skill_name}") + return result + + except VersionConflictError as e: + # Re-raise version conflicts without wrapping + logger.error(f"Version conflict: {e}") + raise + except Exception as e: + logger.error(f"Failed to update registry: {e}") + raise RegistryError(f"Failed to update registry: {e}") + + +def main(): + """Main CLI entry point.""" + if len(sys.argv) < 2: + message = "Usage: registry_update.py [--auto-version]" + response = build_response( + False, + path="", + errors=[message], + details={"error": {"error": "UsageError", "message": message, "details": {}}}, + ) + print(json.dumps(response, indent=2)) + sys.exit(1) + + manifest_path = sys.argv[1] + auto_version = "--auto-version" in sys.argv + + try: + details = update_registry_data(manifest_path, auto_version=auto_version) + response = build_response( + True, + path=details.get("registry_path", REGISTRY_FILE), + errors=[], + details=details, + ) + print(json.dumps(response, indent=2)) + sys.exit(0) + except (RegistryError, VersionConflictError) as e: + logger.error(str(e)) + error_info = format_error_response(e) + + # Check if this is a schema validation error + is_schema_error = "schema validation failed" in str(e).lower() + if is_schema_error: + error_info["type"] = "SchemaError" + + # Mark version conflicts with appropriate error type + if isinstance(e, VersionConflictError): + error_info["type"] = "VersionConflictError" + + response = build_response( + False, + path=manifest_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=manifest_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() diff --git a/skills/registry.update/skill.yaml b/skills/registry.update/skill.yaml new file mode 100644 index 0000000..326fe66 --- /dev/null +++ b/skills/registry.update/skill.yaml @@ -0,0 +1,38 @@ +name: registry.update +version: 0.2.0 +description: > + Updates the Betty Framework Skill Registry by adding or modifying entries + based on validated skill manifests. Supports automatic version bumping based + on semantic versioning rules. +inputs: + - manifest_path + - auto_version +outputs: + - registry_update_result.json +dependencies: + - skill.define + - registry.diff +status: active + +entrypoints: + - command: /registry/update + handler: registry_update.py + runtime: python + description: > + Add or update entries in the Skill Registry with optional automatic version bumping. + parameters: + - name: manifest_path + type: string + required: true + description: Path to the skill manifest (.skill.yaml) being added or updated. + - name: auto_version + type: boolean + required: false + description: > + Enable automatic version bumping based on changes detected. + Rules: field removed → major bump, field/permission added → minor bump, + other changes → patch bump. + permissions: + - filesystem + - read + - write diff --git a/skills/registry.validate/registry_validate.py b/skills/registry.validate/registry_validate.py new file mode 100644 index 0000000..8c141e7 --- /dev/null +++ b/skills/registry.validate/registry_validate.py @@ -0,0 +1,669 @@ +#!/usr/bin/env python3 +""" +Registry Validation Skill for Betty Framework + +Validates registry files for schema compliance, consistency, and completeness. +Provides dry-run mode to test changes before committing. + +Usage: + python registry_validate.py --registry_files '["registry/skills.json"]' +""" + +import argparse +import json +import logging +import os +import sys +from collections import defaultdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Set, Tuple + +try: + from pydantic import ValidationError +except ImportError: + print("Error: pydantic not installed. Run: pip install pydantic", file=sys.stderr) + sys.exit(1) + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +try: + from betty.models import SkillManifest +except ImportError: + # If betty.models not available, define minimal version + class SkillManifest: + pass + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class RegistryValidator: + """Validates Betty Framework registry files.""" + + def __init__(self): + self.errors: List[Dict[str, Any]] = [] + self.warnings: List[Dict[str, Any]] = [] + self.suggestions: List[str] = [] + self.artifact_types_cache: Dict[str, Any] = {} + + def add_error(self, file: str, item: str, field: str, message: str): + """Add an error to the error list.""" + self.errors.append({ + 'file': file, + 'item': item, + 'field': field, + 'message': message, + 'severity': 'error' + }) + logger.error(f"❌ {file} - {item}.{field}: {message}") + + def add_warning(self, file: str, item: str, field: str, message: str): + """Add a warning to the warning list.""" + self.warnings.append({ + 'file': file, + 'item': item, + 'field': field, + 'message': message, + 'severity': 'warning' + }) + logger.warning(f"⚠️ {file} - {item}.{field}: {message}") + + def add_suggestion(self, suggestion: str): + """Add a suggestion for improvement.""" + self.suggestions.append(suggestion) + + def load_artifact_types(self, artifact_types_path: str = "registry/artifact_types.json") -> Dict[str, Any]: + """Load artifact types registry.""" + if self.artifact_types_cache: + return self.artifact_types_cache + + try: + with open(artifact_types_path, 'r') as f: + registry = json.load(f) + self.artifact_types_cache = { + artifact['name']: artifact + for artifact in registry.get('artifact_types', []) + } + return self.artifact_types_cache + except FileNotFoundError: + logger.warning(f"Artifact types registry not found: {artifact_types_path}") + return {} + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in artifact types registry: {e}") + return {} + + def validate_json_syntax(self, file_path: str) -> Tuple[bool, Any]: + """Validate JSON syntax and load data.""" + try: + with open(file_path, 'r') as f: + data = json.load(f) + return True, data + except FileNotFoundError: + self.add_error(file_path, 'file', 'existence', 'File not found') + return False, None + except json.JSONDecodeError as e: + self.add_error(file_path, 'file', 'syntax', f'Invalid JSON: {e}') + return False, None + + def validate_skill(self, skill: Dict[str, Any], file_path: str): + """Validate a single skill against SkillManifest schema.""" + skill_name = skill.get('name', '') + + # Check required fields + required_fields = ['name', 'version', 'description', 'inputs', 'outputs', 'status'] + for field in required_fields: + if field not in skill: + self.add_error(file_path, skill_name, field, f'Missing required field: {field}') + + # Validate with Pydantic if available + if SkillManifest != type: # Check if we have real SkillManifest + try: + SkillManifest(**skill) + except ValidationError as e: + for error in e.errors(): + field = '.'.join(str(loc) for loc in error['loc']) + self.add_error(file_path, skill_name, field, error['msg']) + except Exception as e: + self.add_warning(file_path, skill_name, 'validation', f'Could not validate with Pydantic: {e}') + + def validate_artifact_references( + self, + skill: Dict[str, Any], + file_path: str, + artifact_types: Dict[str, Any] + ): + """Validate artifact type references in skill.""" + skill_name = skill.get('name', '') + + if 'artifact_metadata' not in skill: + return + + metadata = skill['artifact_metadata'] + + # Validate produces + for artifact in metadata.get('produces', []): + artifact_type = artifact.get('type') + if artifact_type and artifact_type not in artifact_types: + self.add_error( + file_path, + skill_name, + f'artifact_metadata.produces.type', + f'Unknown artifact type: {artifact_type}' + ) + + # Validate consumes + for artifact in metadata.get('consumes', []): + if isinstance(artifact, dict): + artifact_type = artifact.get('type') + else: + artifact_type = artifact # String format + + if artifact_type and artifact_type not in artifact_types: + self.add_error( + file_path, + skill_name, + f'artifact_metadata.consumes.type', + f'Unknown artifact type: {artifact_type}' + ) + + def validate_file_paths(self, skill: Dict[str, Any], file_path: str): + """Validate that referenced files exist.""" + skill_name = skill.get('name', '') + + # Check entrypoint handlers + for entrypoint in skill.get('entrypoints', []): + handler = entrypoint.get('handler') + if handler: + handler_path = f"skills/{skill_name}/{handler}" + if not os.path.exists(handler_path): + self.add_error( + file_path, + skill_name, + 'entrypoints.handler', + f'Handler file not found: {handler_path}' + ) + + # Check schemas in artifact_metadata + if 'artifact_metadata' in skill: + for artifact in skill['artifact_metadata'].get('produces', []): + schema = artifact.get('schema') + if schema and not os.path.exists(schema): + self.add_warning( + file_path, + skill_name, + 'artifact_metadata.produces.schema', + f'Schema file not found: {schema}' + ) + + def detect_duplicates(self, items: List[Dict[str, Any]], file_path: str, item_type: str): + """Detect duplicate names/versions in registry.""" + seen = {} + for item in items: + name = item.get('name', '') + version = item.get('version', '') + key = f"{name}:{version}" + + if key in seen: + self.add_error( + file_path, + name, + 'name+version', + f'Duplicate {item_type}: {key}' + ) + seen[key] = True + + def detect_circular_dependencies(self, skills: List[Dict[str, Any]], file_path: str): + """Detect circular dependencies between skills.""" + # Build dependency graph + graph: Dict[str, Set[str]] = defaultdict(set) + + for skill in skills: + skill_name = skill.get('name') + if not skill_name: + continue + + # Check dependencies field + for dep in skill.get('dependencies', []): + if '/' not in dep: # Not a Python package, might be skill dependency + graph[skill_name].add(dep) + + # Find cycles using DFS + visited = set() + rec_stack = set() + cycles = [] + + def dfs(node: str, path: List[str]): + visited.add(node) + rec_stack.add(node) + path.append(node) + + for neighbor in graph.get(node, []): + if neighbor not in visited: + if dfs(neighbor, path.copy()): + return True + elif neighbor in rec_stack: + cycle_start = path.index(neighbor) + cycles.append(path[cycle_start:] + [neighbor]) + return True + + rec_stack.remove(node) + return False + + for skill_name in graph.keys(): + if skill_name not in visited: + dfs(skill_name, []) + + if cycles: + for cycle in cycles: + cycle_str = ' → '.join(cycle) + self.add_error( + file_path, + cycle[0], + 'dependencies', + f'Circular dependency: {cycle_str}' + ) + + def validate_skills_registry( + self, + file_path: str, + check_file_paths: bool, + check_artifacts: bool, + check_dependencies: bool + ) -> Dict[str, Any]: + """Validate skills.json registry.""" + logger.info(f"Validating skills registry: {file_path}") + + # Load and validate JSON + valid, data = self.validate_json_syntax(file_path) + if not valid: + return {'valid': False, 'skill_count': 0} + + skills = data.get('skills', []) + + # Load artifact types if needed + artifact_types = {} + if check_artifacts: + artifact_types = self.load_artifact_types() + + # Detect duplicates + self.detect_duplicates(skills, file_path, 'skill') + + # Validate each skill + skills_with_artifacts = 0 + skills_with_tests = 0 + + for skill in skills: + # Basic validation + self.validate_skill(skill, file_path) + + # Artifact reference validation + if check_artifacts and 'artifact_metadata' in skill: + self.validate_artifact_references(skill, file_path, artifact_types) + skills_with_artifacts += 1 + + # File path validation + if check_file_paths: + self.validate_file_paths(skill, file_path) + + # Check for tests + skill_name = skill.get('name', '') + test_path = f"skills/{skill_name}/test_{skill_name.replace('.', '_')}.py" + if os.path.exists(test_path): + skills_with_tests += 1 + + # Check circular dependencies + if check_dependencies: + self.detect_circular_dependencies(skills, file_path) + + # Generate suggestions + if skills_with_tests < len(skills) / 2: + self.add_suggestion(f"{len(skills) - skills_with_tests} skills missing test coverage") + + if skills_with_artifacts < len(skills) / 2: + self.add_suggestion(f"{len(skills) - skills_with_artifacts} skills missing artifact_metadata") + + return { + 'valid': True, + 'skill_count': len(skills), + 'skills_with_artifacts': skills_with_artifacts, + 'skills_with_tests': skills_with_tests + } + + def validate_agents_registry(self, file_path: str) -> Dict[str, Any]: + """Validate agents.json registry.""" + logger.info(f"Validating agents registry: {file_path}") + + # Load and validate JSON + valid, data = self.validate_json_syntax(file_path) + if not valid: + return {'valid': False, 'agent_count': 0} + + agents = data.get('agents', []) + + # Detect duplicates + self.detect_duplicates(agents, file_path, 'agent') + + # Validate each agent + for agent in agents: + agent_name = agent.get('name', '') + + # Check required fields + required_fields = ['name', 'version', 'description', 'reasoning_mode'] + for field in required_fields: + if field not in agent: + self.add_error(file_path, agent_name, field, f'Missing required field: {field}') + + return { + 'valid': True, + 'agent_count': len(agents) + } + + def validate_artifact_types_registry(self, file_path: str) -> Dict[str, Any]: + """Validate artifact_types.json registry.""" + logger.info(f"Validating artifact types registry: {file_path}") + + # Load and validate JSON + valid, data = self.validate_json_syntax(file_path) + if not valid: + return {'valid': False, 'artifact_type_count': 0} + + artifact_types = data.get('artifact_types', []) + + # Detect duplicates + self.detect_duplicates(artifact_types, file_path, 'artifact type') + + # Validate each artifact type + for artifact_type in artifact_types: + type_name = artifact_type.get('name', '') + + # Check required fields + required_fields = ['name', 'file_pattern', 'content_type'] + for field in required_fields: + if field not in artifact_type: + self.add_error(file_path, type_name, field, f'Missing required field: {field}') + + return { + 'valid': True, + 'artifact_type_count': len(artifact_types) + } + + +def validate_registries( + registry_files: List[str], + check_file_paths: bool = True, + check_artifacts: bool = True, + check_dependencies: bool = True, + strict_mode: bool = False, + output_format: str = "detailed" +) -> Dict[str, Any]: + """ + Validate Betty Framework registry files. + + Args: + registry_files: List of registry file paths + check_file_paths: Whether to verify referenced files exist + check_artifacts: Whether to validate artifact type references + check_dependencies: Whether to check for circular dependencies + strict_mode: Whether to treat warnings as errors + output_format: Output format ("json", "summary", "detailed") + + Returns: + Validation results dictionary + """ + validator = RegistryValidator() + validation_results = {} + start_time = datetime.now(timezone.utc) + + for file_path in registry_files: + if 'skills.json' in file_path: + result = validator.validate_skills_registry( + file_path, check_file_paths, check_artifacts, check_dependencies + ) + elif 'agents.json' in file_path: + result = validator.validate_agents_registry(file_path) + elif 'artifact_types.json' in file_path: + result = validator.validate_artifact_types_registry(file_path) + else: + logger.warning(f"Unknown registry type: {file_path}") + continue + + # Add timing info + result['validated_at'] = datetime.now(timezone.utc).isoformat() + + # Add errors/warnings for this file + file_errors = [e for e in validator.errors if e['file'] == file_path] + file_warnings = [w for w in validator.warnings if w['file'] == file_path] + + result['errors'] = file_errors + result['warnings'] = file_warnings + result['valid'] = len(file_errors) == 0 and (not strict_mode or len(file_warnings) == 0) + + validation_results[file_path] = result + + # Calculate stats + end_time = datetime.now(timezone.utc) + validation_time_ms = int((end_time - start_time).total_seconds() * 1000) + + stats = { + 'validation_time_ms': validation_time_ms + } + + # Aggregate counts + for result in validation_results.values(): + for key in ['skill_count', 'agent_count', 'artifact_type_count']: + if key in result: + stats_key = key.replace('_count', 's') # skill_count → skills + stats[f'total_{stats_key}'] = stats.get(f'total_{stats_key}', 0) + result[key] + + # Overall validity + all_valid = all(result['valid'] for result in validation_results.values()) + + if strict_mode and validator.warnings: + all_valid = False + + return { + 'valid': all_valid, + 'validation_results': validation_results, + 'errors': validator.errors, + 'warnings': validator.warnings, + 'suggestions': validator.suggestions, + 'stats': stats + } + + +def format_output(result: Dict[str, Any], output_format: str) -> str: + """Format validation result based on output_format.""" + if output_format == "json": + return json.dumps(result, indent=2) + + elif output_format == "summary": + lines = ["=== Registry Validation Summary ===\n"] + + for file_path, file_result in result['validation_results'].items(): + status = "✅ Valid" if file_result['valid'] else "❌ Invalid" + error_count = len(file_result.get('errors', [])) + warning_count = len(file_result.get('warnings', [])) + + lines.append(f"{status} {file_path}") + if 'skill_count' in file_result: + lines.append(f" Skills: {file_result['skill_count']}") + if 'agent_count' in file_result: + lines.append(f" Agents: {file_result['agent_count']}") + if 'artifact_type_count' in file_result: + lines.append(f" Artifact Types: {file_result['artifact_type_count']}") + + if error_count: + lines.append(f" Errors: {error_count}") + if warning_count: + lines.append(f" Warnings: {warning_count}") + lines.append("") + + overall = "✅ PASSED" if result['valid'] else "❌ FAILED" + lines.append(f"Overall: {overall}") + lines.append(f"Validation time: {result['stats']['validation_time_ms']}ms") + + return '\n'.join(lines) + + else: # detailed + lines = ["=== Registry Validation Report ===\n"] + + for file_path, file_result in result['validation_results'].items(): + status = "✅ Valid" if file_result['valid'] else "❌ Invalid" + lines.append(f"{file_path}: {status}") + + # Show errors + for error in file_result.get('errors', []): + lines.append(f" ❌ {error['item']}.{error['field']}: {error['message']}") + + # Show warnings + for warning in file_result.get('warnings', []): + lines.append(f" ⚠️ {warning['item']}.{warning['field']}: {warning['message']}") + + lines.append("") + + # Show suggestions + if result['suggestions']: + lines.append("Suggestions:") + for suggestion in result['suggestions']: + lines.append(f" 💡 {suggestion}") + lines.append("") + + overall = "✅ PASSED" if result['valid'] else "❌ FAILED" + lines.append(f"Overall: {overall}") + lines.append(f"Time: {result['stats']['validation_time_ms']}ms") + + return '\n'.join(lines) + + +def main(): + """Main entry point for registry.validate skill.""" + parser = argparse.ArgumentParser( + description="Validate Betty Framework registry files" + ) + + parser.add_argument( + '--registry_files', + type=str, + required=True, + help='JSON array of registry file paths (e.g., \'["registry/skills.json"]\')' + ) + + parser.add_argument( + '--check_file_paths', + type=lambda x: x.lower() == 'true', + default=True, + help='Whether to verify referenced files exist (default: true)' + ) + + parser.add_argument( + '--check_artifacts', + type=lambda x: x.lower() == 'true', + default=True, + help='Whether to validate artifact type references (default: true)' + ) + + parser.add_argument( + '--check_dependencies', + type=lambda x: x.lower() == 'true', + default=True, + help='Whether to check for circular dependencies (default: true)' + ) + + parser.add_argument( + '--strict_mode', + type=lambda x: x.lower() == 'true', + default=False, + help='Whether to treat warnings as errors (default: false)' + ) + + parser.add_argument( + '--output_format', + type=str, + default='detailed', + choices=['json', 'summary', 'detailed'], + help='Output format (default: detailed)' + ) + + parser.add_argument( + '--output', + type=str, + help='Output file path for validation report (optional)' + ) + + args = parser.parse_args() + + try: + # Parse registry_files JSON + registry_files = json.loads(args.registry_files) + + if not isinstance(registry_files, list): + logger.error("registry_files must be a JSON array") + sys.exit(1) + + logger.info(f"Validating {len(registry_files)} registry files...") + + # Perform validation + result = validate_registries( + registry_files=registry_files, + check_file_paths=args.check_file_paths, + check_artifacts=args.check_artifacts, + check_dependencies=args.check_dependencies, + strict_mode=args.strict_mode, + output_format=args.output_format + ) + + # Add status for JSON output + result['ok'] = result['valid'] + result['status'] = 'success' if result['valid'] else 'validation_failed' + + # Format output + output = format_output(result, args.output_format) + + # Save to file if specified + if args.output: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if args.output_format == 'json': + with open(output_path, 'w') as f: + json.dump(result, f, indent=2) + else: + with open(output_path, 'w') as f: + f.write(output) + + logger.info(f"Validation report saved to: {output_path}") + result['validation_report_path'] = str(output_path) + + # Print output + print(output) + + # Exit with appropriate code + sys.exit(0 if result['valid'] else 1) + + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in registry_files parameter: {e}") + print(json.dumps({ + 'ok': False, + 'status': 'error', + 'error': f'Invalid JSON: {str(e)}' + }, indent=2)) + sys.exit(1) + + except Exception as e: + logger.error(f"Unexpected error: {e}", exc_info=True) + print(json.dumps({ + 'ok': False, + 'status': 'error', + 'error': str(e) + }, indent=2)) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/skills/registry.validate/skill.yaml b/skills/registry.validate/skill.yaml new file mode 100644 index 0000000..bcdf476 --- /dev/null +++ b/skills/registry.validate/skill.yaml @@ -0,0 +1,104 @@ +name: registry.validate +version: 0.1.0 +description: > + Validates Betty Framework registry files (skills.json, agents.json, + artifact_types.json) for schema compliance, consistency, and completeness. + Provides dry-run mode to test changes before committing. Checks JSON syntax, + Pydantic model compliance, artifact type references, duplicate detection, + file path existence, and circular dependencies. + +inputs: + - name: registry_files + type: array + required: true + description: List of registry file paths to validate (e.g., ["registry/skills.json", "registry/agents.json"]) + + - name: check_file_paths + type: boolean + required: false + default: true + description: Whether to verify referenced files exist on filesystem + + - name: check_artifacts + type: boolean + required: false + default: true + description: Whether to validate artifact type references against artifact_types.json + + - name: check_dependencies + type: boolean + required: false + default: true + description: Whether to check for circular dependencies between skills/agents + + - name: strict_mode + type: boolean + required: false + default: false + description: Whether to treat warnings as errors (fail on warnings) + + - name: output_format + type: string + required: false + default: detailed + description: Output format - "json", "summary", or "detailed" + +outputs: + - name: valid + type: boolean + description: Whether all registry files passed validation + + - name: validation_results + type: object + description: Detailed validation results for each registry file + + - name: errors + type: array + description: List of errors found across all registries + + - name: warnings + type: array + description: List of warnings found across all registries + + - name: suggestions + type: array + description: List of improvement suggestions + + - name: stats + type: object + description: Statistics about registries (counts, validation time, etc.) + +artifact_metadata: + produces: + - type: validation-report + file_pattern: "*.validation.json" + content_type: application/json + schema: schemas/validation-report.json + description: Registry validation results with errors, warnings, and suggestions + + consumes: [] + +entrypoints: + - command: /registry/validate + handler: registry_validate.py + runtime: python + permissions: + - filesystem:read + +status: active + +tags: + - registry + - validation + - quality + - ci-cd + - testing + - infrastructure + +dependencies: + - pydantic + - jsonschema + - pyyaml + +permissions: + - filesystem:read diff --git a/skills/skill.create/SKILL.md b/skills/skill.create/SKILL.md new file mode 100644 index 0000000..b60096e --- /dev/null +++ b/skills/skill.create/SKILL.md @@ -0,0 +1,45 @@ +--- +name: Skill Create +description: Generates a new Betty Framework Skill directory and manifest. Use when you need to bootstrap a new skill in the Betty Framework. +--- + +# Skill Create + +## Purpose +This skill automates the creation of a new Claude Code-compatible Skill inside the Betty Framework. It scaffolds the directory structure, generates the `skill.yaml` manifest file, and registers the skill in the internal registry. Use this when you want to add a new skill quickly and consistently. + +## Instructions +1. Run the script `skill_create.py` with the following arguments: + ```bash + python skill_create.py "" [--inputs input1,input2] [--outputs output1,output2] +2. The script will create a folder under /skills// with: + * skill.yaml manifest (populated with version 0.1.0 and status draft) + * SKILL.md containing the description + * A registration entry added to registry/skills.json + +3. The new manifest will be validated via the skill.define skill. + +4. After creation, review the generated skill.yaml for correctness, edit if necessary, and then mark status: active when ready for use. + +## Example + +```bash +python skill_create.py workflow.compose "Compose and orchestrate multi-step workflows" --inputs workflow.yaml,context.schema --outputs execution_plan.json +``` + +This will generate: + +``` +skills/ + workflow.compose/ + skill.yaml + README.md +registry/skills.json ← updated with workflow.compose entry +``` + +## Implementation Notes + +* The script uses forward-slash paths (e.g., `skills/workflow.compose/skill.yaml`) to remain cross-platform. +* The manifest file format must include fields: `name`, `version`, `description`, `inputs`, `outputs`, `dependencies`, `status`. +* This skill depends on `skill.define` (for validation) and `context.schema` (for input/output schema support). +* After running, commit the changes to Git; version control provides traceability of new skills. \ No newline at end of file diff --git a/skills/skill.create/__init__.py b/skills/skill.create/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/skill.create/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/skill.create/skill.yaml b/skills/skill.create/skill.yaml new file mode 100644 index 0000000..f3a8724 --- /dev/null +++ b/skills/skill.create/skill.yaml @@ -0,0 +1,47 @@ +name: skill.create +version: 0.1.0 +description: > + Generates a new Betty Framework Skill directory and manifest. + Used to bootstrap new Claude Code-compatible skills inside the Betty Framework. +inputs: + - skill_name + - description + - inputs + - outputs +outputs: + - skill_directory + - manifest_path + - registration_record.json +dependencies: + - skill.define + - context.schema +status: active + +entrypoints: + - command: /skill/create + handler: skill_create.py + runtime: python + description: > + Scaffolds a new Betty Skill directory, generates its manifest, + validates it with skill.define, and updates the registry. + parameters: + - name: skill_name + type: string + description: Name of the new skill (e.g., runtime.execute) + required: true + - name: description + type: string + description: Description of what the skill will do + required: true + - name: inputs + type: string + description: Comma-separated list of input parameters (optional) + required: false + - name: outputs + type: string + description: Comma-separated list of output parameters (optional) + required: false + permissions: + - filesystem + - read + - write diff --git a/skills/skill.create/skill_create.py b/skills/skill.create/skill_create.py new file mode 100644 index 0000000..5a8e4cd --- /dev/null +++ b/skills/skill.create/skill_create.py @@ -0,0 +1,396 @@ +#!/usr/bin/env python3 +""" +Skill Create - Implementation Script +Creates new Claude Code-compatible Skills inside the Betty Framework. + +Usage: + python skill_create.py "" [--inputs input1,input2] [--outputs output1,output2] +""" + +import os +import sys +import yaml +import json +import argparse +import subprocess +from typing import List, Dict, Any, Optional +from datetime import datetime, timezone + + +from betty.config import ( + BASE_DIR, SKILLS_DIR, get_skill_path, get_skill_manifest_path, + get_skill_handler_path, ensure_directories, +) +from betty.enums import SkillStatus +from betty.validation import validate_skill_name, ValidationError +from betty.logging_utils import setup_logger +from betty.errors import SkillNotFoundError, ManifestError, format_error_response + +logger = setup_logger(__name__) + + +def build_response(ok: bool, path: Optional[str], errors: Optional[List[str]] = None, details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Create a standardized response payload for CLI output.""" + + 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 create_skill_manifest( + skill_name: str, + description: str, + inputs: List[str], + outputs: List[str] +) -> Dict[str, Any]: + """ + Create a skill manifest dictionary. + + Args: + skill_name: Name of the skill + description: Description of what the skill does + inputs: List of input parameter names + outputs: List of output parameter names + + Returns: + Skill manifest as dictionary + """ + return { + "name": skill_name, + "version": "0.1.0", + "description": description, + "inputs": inputs, + "outputs": outputs, + "dependencies": [], + "status": SkillStatus.DRAFT.value, + } + + +def write_skill_yaml(manifest_path: str, manifest: Dict[str, Any]) -> None: + """ + Write skill manifest to YAML file. + + Args: + manifest_path: Path to skill.yaml file + manifest: Manifest dictionary + """ + with open(manifest_path, "w") as f: + yaml.dump(manifest, f, sort_keys=False) + logger.info(f"Created manifest: {manifest_path}") + + +def write_skill_md(skill_path: str, skill_name: str, description: str) -> None: + """ + Create minimal SKILL.md documentation file. + + Args: + skill_path: Path to skill directory + skill_name: Name of the skill + description: Description of the skill + """ + skill_md_path = os.path.join(skill_path, "SKILL.md") + content = f"""--- +name: {skill_name} +description: {description} +--- + +# {skill_name} + +{description} + +## Status + +Auto-generated via `skill.create`. + +## Usage + +TODO: Add usage instructions + +## Inputs + +TODO: Document inputs + +## Outputs + +TODO: Document outputs + +## Dependencies + +TODO: List dependencies +""" + with open(skill_md_path, "w") as f: + f.write(content) + logger.info(f"Created documentation: {skill_md_path}") + + +def create_skill_handler(skill_path: str, skill_name: str) -> None: + """ + Create a minimal skill handler Python script. + + Args: + skill_path: Path to skill directory + skill_name: Name of the skill + """ + handler_name = skill_name.replace('.', '_') + '.py' + handler_path = os.path.join(skill_path, handler_name) + + content = f"""#!/usr/bin/env python3 +\"\"\" +{skill_name} - Implementation Script +Auto-generated by skill.create +\"\"\" + +import os +import sys +import json +import argparse + +# Add Betty framework to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response + +logger = setup_logger(__name__) + + +def main(): + \"\"\"Main entry point for {skill_name}.\"\"\" + parser = argparse.ArgumentParser(description="{skill_name}") + # TODO: Add arguments + args = parser.parse_args() + + try: + logger.info("Executing {skill_name}...") + # TODO: Implement skill logic + result = {{"status": "success", "message": "Not yet implemented"}} + print(json.dumps(result, indent=2)) + except Exception as e: + logger.error(f"Error executing {skill_name}: {{e}}") + print(json.dumps(format_error_response(e), indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() +""" + with open(handler_path, "w") as f: + f.write(content) + os.chmod(handler_path, 0o755) # Make executable + logger.info(f"Created handler: {handler_path}") + + +def run_validator(manifest_path: str) -> bool: + """ + Run skill.define validator if available. + + Args: + manifest_path: Path to skill manifest + + Returns: + True if validation succeeded or validator not found, False if validation failed + """ + validator_path = os.path.join(BASE_DIR, "skills", "skill.define", "skill_define.py") + + if not os.path.exists(validator_path): + logger.warning("skill_define.py not found — skipping validation.") + return True + + logger.info(f"Validating new skill with skill.define...") + result = subprocess.run( + [sys.executable, validator_path, manifest_path], + capture_output=True, + text=True + ) + + if result.returncode != 0: + logger.error(f"Validation failed: {result.stderr}") + return False + + logger.info("Validation succeeded") + return True + + +def update_registry(manifest_path: str) -> bool: + """ + Update registry using 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 not found — skipping registry update.") + return False + + logger.info("Updating registry via registry.update...") + 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 updated successfully") + return True + + +def create_skill( + skill_name: str, + description: str, + inputs: List[str], + outputs: List[str] +) -> Dict[str, Any]: + """ + Scaffold a new skill directory and manifest. + + Args: + skill_name: Name of the new skill + description: Description of what the skill does + inputs: List of input parameter names + outputs: List of output parameter names + + Returns: + Result dictionary with status and created file paths + + Raises: + ValidationError: If skill_name is invalid + ManifestError: If skill already exists or creation fails + """ + # Validate skill name + validate_skill_name(skill_name) + + # Ensure directories exist + ensure_directories() + + # Check if skill already exists + skill_path = get_skill_path(skill_name) + if os.path.exists(skill_path): + raise ManifestError( + f"Skill '{skill_name}' already exists", + details={"skill_path": skill_path} + ) + + try: + # Create skill directory + os.makedirs(skill_path, exist_ok=True) + logger.info(f"Created skill directory: {skill_path}") + + # Create manifest + manifest = create_skill_manifest(skill_name, description, inputs, outputs) + manifest_path = get_skill_manifest_path(skill_name) + write_skill_yaml(manifest_path, manifest) + + # Create SKILL.md + write_skill_md(skill_path, skill_name, description) + + # Create handler script + create_skill_handler(skill_path, skill_name) + + # Validate + validation_success = run_validator(manifest_path) + + # Update registry + registry_success = update_registry(manifest_path) + + result = { + "status": "success", + "skill_name": skill_name, + "skill_path": skill_path, + "manifest_path": manifest_path, + "validation": "passed" if validation_success else "skipped", + "registry_updated": registry_success, + "timestamp": datetime.now(timezone.utc).isoformat() + } + + logger.info(f"✅ Successfully created skill: {skill_name}") + return result + + except Exception as e: + logger.error(f"Failed to create skill: {e}") + raise ManifestError(f"Failed to create skill: {e}") + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description="Create a new Betty Framework Skill.", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "skill_name", + help="Name of the new skill (e.g., runtime.execute)" + ) + parser.add_argument( + "description", + help="Description of what the skill does." + ) + parser.add_argument( + "--inputs", + help="Comma-separated list of inputs", + default="" + ) + parser.add_argument( + "--outputs", + help="Comma-separated list of outputs", + default="" + ) + + args = parser.parse_args() + + inputs = [i.strip() for i in args.inputs.split(",") if i.strip()] + outputs = [o.strip() for o in args.outputs.split(",") if o.strip()] + + try: + details = create_skill(args.skill_name, args.description, inputs, outputs) + response = build_response( + True, + path=details.get("skill_path"), + errors=[], + details=details, + ) + print(json.dumps(response, indent=2)) + sys.exit(0) + except (ValidationError, ManifestError) as e: + logger.error(str(e)) + error_info = format_error_response(e) + path = None + if isinstance(e, ManifestError): + path = e.details.get("skill_path") or e.details.get("manifest_path") + 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=None, + errors=[error_info.get("message", str(e))], + details={"error": error_info}, + ) + print(json.dumps(response, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/skill.define/SKILL.md b/skills/skill.define/SKILL.md new file mode 100644 index 0000000..ee55ef3 --- /dev/null +++ b/skills/skill.define/SKILL.md @@ -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 +``` + +### 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 diff --git a/skills/skill.define/__init__.py b/skills/skill.define/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/skill.define/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/skill.define/skill.yaml b/skills/skill.define/skill.yaml new file mode 100644 index 0000000..0305534 --- /dev/null +++ b/skills/skill.define/skill.yaml @@ -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 diff --git a/skills/skill.define/skill_define.py b/skills/skill.define/skill_define.py new file mode 100644 index 0000000..f1b14ad --- /dev/null +++ b/skills/skill.define/skill_define.py @@ -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 " + 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() diff --git a/skills/story.write/SKILL.md b/skills/story.write/SKILL.md new file mode 100644 index 0000000..f1d6975 --- /dev/null +++ b/skills/story.write/SKILL.md @@ -0,0 +1,87 @@ +# story.write + +Convert decomposed items from epic.decompose into fully formatted user stories. Generates individual Markdown files for each story following standard user story format. + +## Overview + +**Purpose:** Convert decomposed items from epic.decompose into fully formatted user stories. Generates individual Markdown files for each story following standard user story format. + +**Command:** `/story/write` + +## Usage + +### Basic Usage + +```bash +python3 skills/story/write/story_write.py +``` + +### With Arguments + +```bash +python3 skills/story/write/story_write.py \ + --stories_file_(string,_required):_path_to_the_stories.json_file_from_epic.decompose "value" \ + --epic_reference_(string,_optional):_reference_to_the_source_epic_for_traceability "value" \ + --output_dir_(string,_optional):_directory_to_save_story_files_(default:_./stories/) "value" \ + --output-format json +``` + +## Inputs + +- **stories_file (string, required): Path to the stories.json file from epic.decompose** +- **epic_reference (string, optional): Reference to the source Epic for traceability** +- **output_dir (string, optional): Directory to save story files (default: ./stories/)** + +## Outputs + +- **story_.md: Markdown file per story with persona, goal, benefit, acceptance criteria, and metadata** +- **stories_index.md: Summary index of all created stories** + +## Artifact Metadata + +### Consumes + +- `user-stories-list` + +### Produces + +- `user-story` + +## Examples + +- python3 skills/story.write/story_write.py --stories-file ./stories.json --epic-reference "EPIC-001" --output-dir ./stories/ + +## Permissions + +- `filesystem:read` +- `filesystem:write` + +## Implementation Notes + +Load and parse stories.json. Generate unique story ID for each item. Format as standard user story (As a/I want/So that). Convert acceptance criteria to checklist format. Add traceability links to Epic. Include metadata for tracking. Create summary index file listing all stories. Support batch processing. Validate each story against INVEST criteria. + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - story.write +``` + +## Testing + +Run tests with: + +```bash +pytest skills/story/write/test_story_write.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/story.write/__init__.py b/skills/story.write/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/story.write/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/story.write/skill.yaml b/skills/story.write/skill.yaml new file mode 100644 index 0000000..32ab254 --- /dev/null +++ b/skills/story.write/skill.yaml @@ -0,0 +1,29 @@ +name: story.write +version: 0.1.0 +description: Convert decomposed items from epic.decompose into fully formatted user + stories. Generates individual Markdown files for each story following standard user + story format. +inputs: +- 'stories_file (string, required): Path to the stories.json file from epic.decompose' +- 'epic_reference (string, optional): Reference to the source Epic for traceability' +- 'output_dir (string, optional): Directory to save story files (default: ./stories/)' +outputs: +- 'story_.md: Markdown file per story with persona, goal, benefit, acceptance criteria, + and metadata' +- 'stories_index.md: Summary index of all created stories' +status: active +permissions: +- filesystem:read +- filesystem:write +entrypoints: +- command: /story/write + handler: story_write.py + runtime: python + description: Convert decomposed items from epic.decompose into fully formatted user + stories. Generates individual +artifact_metadata: + produces: + - type: user-story + consumes: + - type: user-stories-list + required: true diff --git a/skills/story.write/story_write.py b/skills/story.write/story_write.py new file mode 100755 index 0000000..2c7c2a6 --- /dev/null +++ b/skills/story.write/story_write.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +""" +story.write - Convert decomposed items from epic.decompose into fully formatted user stories. Generates individual Markdown files for each story following standard user story format. + +Generated by meta.skill with Betty Framework certification +""" + +import os +import sys +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional +from datetime import datetime +import hashlib + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.certification import certified_skill + +logger = setup_logger(__name__) + + +class StoryWrite: + """ + Convert decomposed items from epic.decompose into fully formatted user stories. Generates individual Markdown files for each story following standard user story format. + """ + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + @certified_skill("story.write") + def execute(self, stories_file: Optional[str] = None, epic_reference: Optional[str] = None, + output_dir: Optional[str] = None) -> Dict[str, Any]: + """ + Execute the skill + + Args: + stories_file: Path to the stories.json file from epic.decompose + epic_reference: Reference to the source Epic for traceability + output_dir: Directory to save story files (default: ./stories/) + + Returns: + Dict with execution results + """ + try: + logger.info("Executing story.write...") + + # Validate required inputs + if not stories_file: + raise ValueError("stories_file is required") + + # Set defaults + if not output_dir: + output_dir = "./stories/" + + # Read stories JSON file + stories_path = Path(stories_file) + if not stories_path.exists(): + raise FileNotFoundError(f"Stories file not found: {stories_file}") + + with open(stories_path, 'r') as f: + stories = json.load(f) + + if not isinstance(stories, list): + raise ValueError("Stories file must contain a JSON array") + + # Create output directory + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + # Generate story files + created_files = [] + story_index = [] + + for i, story_data in enumerate(stories, start=1): + story_id = self._generate_story_id(story_data, i) + filename = f"story_{i:03d}.md" + filepath = output_path / filename + + # Generate story content + story_content = self._generate_story_markdown( + story_data, + story_id, + i, + epic_reference + ) + + # Write story file + filepath.write_text(story_content) + created_files.append(str(filepath)) + + # Add to index + story_index.append({ + "id": story_id, + "number": i, + "title": story_data.get("title", f"Story {i}"), + "file": filename + }) + + logger.info(f"Created story {i}: {filename}") + + # Create index file + index_file = output_path / "stories_index.md" + index_content = self._generate_index(story_index, epic_reference) + index_file.write_text(index_content) + created_files.append(str(index_file)) + + logger.info(f"Created {len(stories)} story files in {output_dir}") + + result = { + "ok": True, + "status": "success", + "message": f"Generated {len(stories)} user story documents", + "output_dir": str(output_dir), + "created_files": created_files, + "story_count": len(stories), + "artifact_type": "user-story", + "index_file": str(index_file) + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + def _generate_story_id(self, story_data: Dict[str, Any], number: int) -> str: + """ + Generate unique story ID + + Args: + story_data: Story data dictionary + number: Story number + + Returns: + Story ID string + """ + # Generate ID from story title hash + number + title = story_data.get("title", f"Story {number}") + title_hash = hashlib.md5(title.encode()).hexdigest()[:8] + return f"STORY-{number:03d}-{title_hash.upper()}" + + def _generate_story_markdown(self, story_data: Dict[str, Any], story_id: str, + number: int, epic_reference: Optional[str]) -> str: + """ + Generate markdown content for a user story + + Args: + story_data: Story data dictionary + story_id: Unique story ID + number: Story number + epic_reference: Reference to source Epic + + Returns: + Formatted markdown content + """ + title = story_data.get("title", f"Story {number}") + persona = story_data.get("persona", "As a User") + goal = story_data.get("goal", "achieve a goal") + benefit = story_data.get("benefit", "So that value is delivered") + acceptance_criteria = story_data.get("acceptance_criteria", []) + + # Format acceptance criteria as checklist + ac_list = '\n'.join(f"- [ ] {ac}" for ac in acceptance_criteria) + + # Build markdown content + markdown = f"""# User Story: {title} + +## Story ID +{story_id} + +## User Story + +{persona} +I want to {goal} +{benefit} + +## Acceptance Criteria + +{ac_list} +""" + + # Add Epic reference if provided + if epic_reference: + markdown += f""" +## Linked Epic + +{epic_reference} +""" + + # Add metadata section + markdown += f""" +## Metadata + +- **Story ID**: {story_id} +- **Story Number**: {number} +- **Created**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +- **Status**: Draft +- **Priority**: TBD +- **Estimate**: TBD + +## INVEST Criteria Check + +- [ ] **Independent**: Can this story be developed independently? +- [ ] **Negotiable**: Is there room for discussion on implementation? +- [ ] **Valuable**: Does this deliver value to users/stakeholders? +- [ ] **Estimable**: Can the team estimate the effort required? +- [ ] **Small**: Is this small enough to fit in a sprint? +- [ ] **Testable**: Can we define clear tests for this story? + +## Notes + +_Add implementation notes, technical considerations, or dependencies here._ + +--- + +**Generated by**: Betty Framework - epic-to-story skill chain +**Artifact Type**: user-story +**Version**: 1.0 +""" + + return markdown + + def _generate_index(self, story_index: List[Dict[str, Any]], + epic_reference: Optional[str]) -> str: + """ + Generate index/summary file for all stories + + Args: + story_index: List of story metadata + epic_reference: Reference to source Epic + + Returns: + Index markdown content + """ + markdown = f"""# User Stories Index + +**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +**Total Stories**: {len(story_index)} +""" + + if epic_reference: + markdown += f"**Source Epic**: {epic_reference}\n" + + markdown += """ +## Story List + +""" + + # Add table of stories + markdown += "| # | Story ID | Title | File |\n" + markdown += "|---|----------|-------|------|\n" + + for story in story_index: + markdown += f"| {story['number']} | {story['id']} | {story['title']} | [{story['file']}](./{story['file']}) |\n" + + markdown += """ +## Progress Tracking + +- [ ] All stories reviewed and refined +- [ ] Story priorities assigned +- [ ] Story estimates completed +- [ ] Stories added to backlog +- [ ] Sprint planning completed + +## Notes + +This index was automatically generated from the Epic decomposition process. Review each story to ensure it meets INVEST criteria and refine as needed before adding to the sprint backlog. + +--- + +**Generated by**: Betty Framework - epic-to-story skill chain +""" + + return markdown + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="Convert decomposed items from epic.decompose into fully formatted user stories. Generates individual Markdown files for each story following standard user story format." + ) + + parser.add_argument( + "--stories-file", + required=True, + help="Path to the stories.json file from epic.decompose" + ) + parser.add_argument( + "--epic-reference", + help="Reference to the source Epic for traceability" + ) + parser.add_argument( + "--output-dir", + default="./stories/", + help="Directory to save story files (default: ./stories/)" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = StoryWrite() + + # Execute skill + result = skill.execute( + stories_file=args.stories_file, + epic_reference=args.epic_reference, + output_dir=args.output_dir, + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/story.write/test_story_write.py b/skills/story.write/test_story_write.py new file mode 100644 index 0000000..b93a5d6 --- /dev/null +++ b/skills/story.write/test_story_write.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for story.write + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.story_write import story_write + + +class TestStoryWrite: + """Tests for StoryWrite""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = story_write.StoryWrite() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["story_write.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + story_write.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Convert decomposed items from epic.decompose into " in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/telemetry.capture/INTEGRATION_EXAMPLES.md b/skills/telemetry.capture/INTEGRATION_EXAMPLES.md new file mode 100644 index 0000000..a725f77 --- /dev/null +++ b/skills/telemetry.capture/INTEGRATION_EXAMPLES.md @@ -0,0 +1,448 @@ +# Telemetry Integration Examples + +This document provides practical examples of integrating telemetry tracking into Betty Framework CLI entrypoints, workflows, and agents. + +## Quick Start + +The Betty Framework provides two main approaches for telemetry integration: + +1. **Decorator Pattern** - For CLI entrypoints with standard main() functions +2. **Manual Capture** - For workflows, agents, and custom integrations + +## 1. Decorator Pattern (Recommended for CLI) + +### Standard CLI with Return Code + +For CLI entrypoints that return an exit code: + +```python +#!/usr/bin/env python3 +import sys +import os +from typing import Optional, List + +# Ensure project root on path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from betty.logging_utils import setup_logger +from betty.telemetry_integration import telemetry_tracked + +logger = setup_logger(__name__) + +@telemetry_tracked(skill_name="my.skill", caller="cli") +def main(argv: Optional[List[str]] = None) -> int: + """Entry point for CLI execution.""" + argv = argv or sys.argv[1:] + + # Your skill logic here + if not argv: + logger.error("Missing required arguments") + return 1 + + # Process and return success + return 0 + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) +``` + +**What the decorator does:** +- Automatically measures execution time +- Captures success/failure based on return code (0 = success, non-zero = failed) +- Logs telemetry with sanitized inputs +- Non-blocking - telemetry failures don't affect the main function + +### Example: workflow.validate Integration + +The `workflow.validate` skill has been updated with telemetry tracking: + +```python +# skills/workflow.validate/workflow_validate.py +from betty.telemetry_integration import telemetry_tracked + +@telemetry_tracked(skill_name="workflow.validate", caller="cli") +def main(argv: Optional[List[str]] = None) -> int: + """Entry point for CLI execution.""" + # ... existing validation logic ... + return 0 if response["ok"] else 1 +``` + +**Test it:** +```bash +python3 skills/workflow.validate/workflow_validate.py example.yaml +cat registry/telemetry.json +``` + +## 2. Manual Capture Pattern + +### Direct Function Call + +For programmatic telemetry capture: + +```python +from skills.telemetry.capture.telemetry_capture import create_telemetry_entry, capture_telemetry +import time + +# Track execution manually +start_time = time.time() +try: + # Your logic here + result = execute_my_skill(param1, param2) + status = "success" +except Exception as e: + status = "failed" + raise +finally: + duration_ms = int((time.time() - start_time) * 1000) + + entry = create_telemetry_entry( + skill_name="my.skill", + inputs={"param1": param1, "param2": param2}, + status=status, + duration_ms=duration_ms, + caller="api" + ) + capture_telemetry(entry) +``` + +### Workflow Execution Tracking + +For capturing telemetry within workflow execution: + +```python +# In workflow.compose or similar workflow executor +from betty.telemetry_integration import track_skill_execution + +def execute_workflow_step(step_config: dict, workflow_name: str): + """Execute a workflow step with telemetry tracking.""" + + skill_name = step_config["skill"] + skill_args = step_config["args"] + + result = track_skill_execution( + skill_name=skill_name, + func=lambda: run_skill(skill_name, skill_args), + inputs={"args": skill_args}, + workflow=workflow_name, + caller="workflow" + ) + + return result +``` + +### Agent Skill Invocation + +For tracking skills invoked by agents: + +```python +# In agent skill runner +from betty.telemetry_integration import track_skill_execution + +def run_agent_skill(agent_name: str, skill_name: str, **kwargs): + """Run a skill on behalf of an agent with telemetry.""" + + result = track_skill_execution( + skill_name=skill_name, + func=lambda: execute_skill(skill_name, **kwargs), + inputs=kwargs, + agent=agent_name, + caller="agent" + ) + + return result +``` + +## 3. CLI Helper Function + +For simple CLI telemetry without decorators: + +```python +from betty.telemetry_integration import capture_cli_telemetry +import time + +def main(): + start_time = time.time() + status = "failed" + + try: + # Your CLI logic + process_command() + status = "success" + return 0 + except Exception as e: + logger.error(f"Error: {e}") + return 1 + finally: + duration_ms = int((time.time() - start_time) * 1000) + capture_cli_telemetry( + skill_name="my.skill", + inputs={"cli_args": sys.argv[1:]}, + status=status, + duration_ms=duration_ms, + caller="cli" + ) +``` + +## 4. Context-Rich Telemetry + +### Full Context Example + +Capture comprehensive context for deep analytics: + +```python +entry = create_telemetry_entry( + skill_name="agent.define", + inputs={ + "name": "my-agent", + "mode": "iterative", + "capabilities": ["code-gen", "testing"] + }, + status="success", + duration_ms=2500, + agent="meta-orchestrator", # Which agent invoked this + workflow="agent-creation-pipeline", # Which workflow this is part of + caller="api", # How it was invoked + + # Custom fields for advanced analytics + user_id="dev-123", + environment="staging", + version="1.0.0" +) + +capture_telemetry(entry) +``` + +## 5. Batch Operations + +For tracking multiple skill executions in batch: + +```python +from skills.telemetry.capture.telemetry_capture import create_telemetry_entry, capture_telemetry +import time + +def process_batch(items: list): + """Process items in batch with per-item telemetry.""" + + for item in items: + start_time = time.time() + + try: + process_item(item) + status = "success" + except Exception as e: + status = "failed" + + duration_ms = int((time.time() - start_time) * 1000) + + # Capture telemetry for each item + entry = create_telemetry_entry( + skill_name="batch.processor", + inputs={"item_id": item.id}, + status=status, + duration_ms=duration_ms, + caller="batch" + ) + capture_telemetry(entry) +``` + +## 6. Error Handling Best Practices + +### Graceful Telemetry Failures + +Always wrap telemetry in try-except to prevent failures from affecting main logic: + +```python +def my_critical_function(): + """Critical function that must not fail due to telemetry issues.""" + + start_time = time.time() + result = None + + try: + # Critical business logic + result = perform_critical_operation() + status = "success" + except Exception as e: + status = "failed" + raise # Re-raise the original error + finally: + # Capture telemetry (failures are logged but don't interrupt) + try: + duration_ms = int((time.time() - start_time) * 1000) + capture_cli_telemetry( + skill_name="critical.operation", + inputs={}, + status=status, + duration_ms=duration_ms + ) + except Exception as telemetry_error: + logger.warning(f"Telemetry capture failed: {telemetry_error}") + # Don't raise - telemetry is not critical + + return result +``` + +## 7. Sensitive Data Protection + +### Sanitize Inputs + +Never capture sensitive data like passwords, tokens, or PII: + +```python +def sanitize_inputs(inputs: dict) -> dict: + """Remove sensitive fields from inputs before logging.""" + + sensitive_keys = ["password", "token", "api_key", "secret", "ssn", "credit_card"] + + sanitized = {} + for key, value in inputs.items(): + if any(sensitive in key.lower() for sensitive in sensitive_keys): + sanitized[key] = "***REDACTED***" + else: + sanitized[key] = value + + return sanitized + +# Use it +entry = create_telemetry_entry( + skill_name="auth.login", + inputs=sanitize_inputs({"username": "john", "password": "secret123"}), + status="success", + duration_ms=150 +) +``` + +## 8. Testing with Telemetry + +### Unit Test Example + +```python +import unittest +from unittest.mock import patch, MagicMock + +class TestMySkillWithTelemetry(unittest.TestCase): + + @patch('betty.telemetry_integration.capture_cli_telemetry') + def test_skill_execution_captures_telemetry(self, mock_capture): + """Test that telemetry is captured on skill execution.""" + + # Execute the skill + from skills.my.skill.my_skill import main + result = main(["arg1", "arg2"]) + + # Verify telemetry was captured + self.assertEqual(result, 0) + mock_capture.assert_called_once() + + # Verify telemetry parameters + call_args = mock_capture.call_args + self.assertEqual(call_args.kwargs['skill_name'], "my.skill") + self.assertEqual(call_args.kwargs['status'], "success") + self.assertGreater(call_args.kwargs['duration_ms'], 0) +``` + +## 9. Migration Checklist + +To add telemetry to an existing skill: + +1. **Import the telemetry integration:** + ```python + from betty.telemetry_integration import telemetry_tracked + ``` + +2. **Apply the decorator to main():** + ```python + @telemetry_tracked(skill_name="your.skill", caller="cli") + def main(argv: Optional[List[str]] = None) -> int: + ``` + +3. **Ensure main() returns an int:** + - Return 0 for success + - Return non-zero for failure + +4. **Test the integration:** + ```bash + python3 skills/your.skill/your_skill.py test-args + cat registry/telemetry.json | jq '.[-1]' # View latest entry + ``` + +## 10. Querying Telemetry Data + +### Basic Queries + +```bash +# Count total telemetry entries +jq 'length' registry/telemetry.json + +# Find all failed executions +jq '.[] | select(.status == "failed")' registry/telemetry.json + +# Get average duration for a skill +jq '[.[] | select(.skill == "workflow.validate") | .duration_ms] | add / length' registry/telemetry.json + +# Top 10 slowest executions +jq 'sort_by(.duration_ms) | reverse | .[0:10]' registry/telemetry.json + +# Executions by caller +jq 'group_by(.caller) | map({caller: .[0].caller, count: length})' registry/telemetry.json +``` + +### Advanced Analytics + +```bash +# Skills executed in last hour +jq --arg cutoff "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S)" \ + '.[] | select(.timestamp > $cutoff)' registry/telemetry.json + +# Success rate by skill +jq 'group_by(.skill) | map({ + skill: .[0].skill, + total: length, + successful: ([.[] | select(.status == "success")] | length), + success_rate: (([.[] | select(.status == "success")] | length) / length * 100) +})' registry/telemetry.json +``` + +## 11. Future: Prometheus Export + +Example of how telemetry data could be exported to Prometheus (future implementation): + +```python +# Future: skills/telemetry.capture/exporters/prometheus.py +from prometheus_client import Counter, Histogram, Gauge + +skill_executions = Counter( + 'betty_skill_executions_total', + 'Total skill executions', + ['skill', 'status', 'caller'] +) + +skill_duration = Histogram( + 'betty_skill_duration_seconds', + 'Skill execution duration', + ['skill', 'caller'] +) + +def export_to_prometheus(telemetry_entry: dict): + """Export telemetry entry to Prometheus metrics.""" + skill_executions.labels( + skill=telemetry_entry['skill'], + status=telemetry_entry['status'], + caller=telemetry_entry.get('caller', 'unknown') + ).inc() + + skill_duration.labels( + skill=telemetry_entry['skill'], + caller=telemetry_entry.get('caller', 'unknown') + ).observe(telemetry_entry['duration_ms'] / 1000.0) +``` + +## Summary + +- **CLI Skills**: Use `@telemetry_tracked` decorator +- **Workflows**: Use `track_skill_execution()` helper +- **Custom Code**: Use `create_telemetry_entry()` + `capture_telemetry()` +- **Always**: Handle telemetry failures gracefully +- **Never**: Capture sensitive data in inputs + +For more details, see the [SKILL.md](SKILL.md) documentation. diff --git a/skills/telemetry.capture/SKILL.md b/skills/telemetry.capture/SKILL.md new file mode 100644 index 0000000..4318808 --- /dev/null +++ b/skills/telemetry.capture/SKILL.md @@ -0,0 +1,392 @@ +# telemetry.capture + +**Version:** 0.1.0 +**Status:** Active +**Tags:** telemetry, logging, observability, audit + +## Overview + +The `telemetry.capture` skill provides comprehensive execution logging for Betty Framework components. It captures usage metrics, execution status, timing data, and contextual metadata in a structured, thread-safe manner. + +All telemetry data is written to `/registry/telemetry.json` with ISO timestamps and validated JSON schema. + +## Features + +- Thread-safe JSON logging using file locking (fcntl) +- ISO 8601 timestamp formatting with timezone support +- Structured telemetry entries with validation +- Query interface for telemetry analysis +- Decorator pattern for automatic capture (`@capture_telemetry`) +- Context manager pattern for manual capture +- CLI and programmatic interfaces +- Input sanitization (exclude secrets) + +## Purpose + +This skill enables: +- **Observability**: Track execution patterns across Betty components +- **Performance Monitoring**: Measure duration of skill executions +- **Error Tracking**: Capture failures with detailed error messages +- **Usage Analytics**: Understand which skills are used most frequently +- **Audit Trail**: Maintain compliance and debugging history +- **Workflow Analysis**: Trace caller chains and dependencies + +## Usage + +### Basic CLI Usage + +```bash +# Capture a successful execution +python skills/telemetry.capture/telemetry_capture.py plugin.build success 320 CLI + +# Capture with inputs +python skills/telemetry.capture/telemetry_capture.py \ + agent.run success 1500 API '{"agent": "api.designer", "task": "design_api"}' + +# Capture a failure +python skills/telemetry.capture/telemetry_capture.py \ + workflow.compose failure 2800 CLI '{"workflow": "api_first"}' "Validation failed at step 3" +``` + +### As a Decorator (Recommended) + +```python +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent / "../telemetry.capture")) + +from telemetry_utils import capture_telemetry + +@capture_telemetry(skill_name="agent.run", caller="CLI", capture_inputs=True) +def run_agent(agent_path: str, task_context: str = None): + """Execute a Betty agent.""" + # ... implementation + return {"status": "success", "result": execution_result} + +# Usage +result = run_agent("/agents/api.designer", "Design user authentication API") +# Telemetry is automatically captured +``` + +### As a Context Manager + +```python +from telemetry_utils import TelemetryContext + +def build_plugin(plugin_path: str): + with TelemetryContext(skill="plugin.build", caller="CLI") as ctx: + ctx.set_inputs({"plugin_path": plugin_path}) + + try: + # Perform build operations + result = create_plugin_archive(plugin_path) + ctx.set_status("success") + return result + except Exception as e: + ctx.set_error(str(e)) + raise +``` + +### Programmatic API + +```python +from telemetry_capture import TelemetryCapture + +telemetry = TelemetryCapture() + +# Capture an event +entry = telemetry.capture( + skill="plugin.build", + status="success", + duration_ms=320.5, + caller="CLI", + inputs={"plugin_path": "./plugin.yaml", "output_format": "tar.gz"}, + metadata={"user": "developer@example.com", "environment": "production"} +) + +print(f"Captured: {entry['timestamp']}") +``` + +### Query Telemetry Data + +```python +from telemetry_capture import TelemetryCapture + +telemetry = TelemetryCapture() + +# Query recent failures +failures = telemetry.query(status="failure", limit=10) + +# Query specific skill usage +agent_runs = telemetry.query(skill="agent.run", limit=50) + +# Query by caller +cli_executions = telemetry.query(caller="CLI", limit=100) + +for entry in failures: + print(f"{entry['timestamp']}: {entry['skill']} - {entry['error_message']}") +``` + +## Parameters + +### Capture Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `skill` | string | Yes | Name of the skill/component (e.g., 'plugin.build') | +| `status` | string | Yes | Execution status: success, failure, timeout, error, pending | +| `duration_ms` | number | Yes | Execution duration in milliseconds | +| `caller` | string | Yes | Source of the call (CLI, API, workflow.compose) | +| `inputs` | object | No | Sanitized input parameters (default: {}) | +| `error_message` | string | No | Error message if status is failure/error | +| `metadata` | object | No | Additional context (user, session_id, environment) | + +### Decorator Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `skill_name` | string | function name | Override skill name | +| `caller` | string | "runtime" | Caller identifier | +| `capture_inputs` | boolean | False | Whether to capture function arguments | +| `sanitize_keys` | list | None | Parameter names to redact (e.g., ['password']) | + +## Output Format + +### Telemetry Entry Structure + +```json +{ + "timestamp": "2025-10-24T14:30:45.123456+00:00", + "skill": "plugin.build", + "status": "success", + "duration_ms": 320.5, + "caller": "CLI", + "inputs": { + "plugin_path": "./plugin.yaml", + "output_format": "tar.gz" + }, + "error_message": null, + "metadata": { + "user": "developer@example.com", + "environment": "production" + } +} +``` + +### Telemetry File Structure + +```json +[ + { + "timestamp": "2025-10-24T14:30:45.123456+00:00", + "skill": "plugin.build", + "status": "success", + "duration_ms": 320.5, + "caller": "CLI", + "inputs": { + "plugin_path": "./plugin.yaml", + "output_format": "tar.gz" + }, + "error_message": null, + "metadata": {} + }, + { + "timestamp": "2025-10-24T14:32:10.789012+00:00", + "skill": "agent.run", + "status": "success", + "duration_ms": 1500.0, + "caller": "API", + "inputs": { + "agent": "api.designer" + }, + "error_message": null, + "metadata": {} + } +] +``` + +Note: The telemetry file is a simple JSON array for efficient querying and compatibility with existing Betty Framework tools. + +## Examples + +### Example 1: Capture Plugin Build + +```bash +python skills/telemetry.capture/telemetry_capture.py \ + plugin.build success 320 CLI '{"plugin_path": "./plugin.yaml", "output_format": "tar.gz"}' +``` + +**Output:** +```json +{ + "timestamp": "2025-10-24T14:30:45.123456+00:00", + "skill": "plugin.build", + "status": "success", + "duration_ms": 320.0, + "caller": "CLI", + "inputs": { + "plugin_path": "./plugin.yaml", + "output_format": "tar.gz" + }, + "error_message": null, + "metadata": {} +} + +✓ Telemetry captured to /home/user/betty/registry/telemetry.json +``` + +### Example 2: Capture Agent Execution with Decorator + +```python +# In skills/agent.run/agent_run.py +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent / "../telemetry.capture")) + +from telemetry_utils import capture_telemetry + +@capture_telemetry( + skill_name="agent.run", + caller="CLI", + capture_inputs=True, + sanitize_keys=["api_key", "password"] +) +def main(): + """Execute agent with automatic telemetry capture.""" + # ... existing implementation + return {"status": "success", "result": result} + +if __name__ == "__main__": + main() +``` + +### Example 3: Query Recent Failures + +```python +from telemetry_capture import TelemetryCapture + +telemetry = TelemetryCapture() +failures = telemetry.query(status="failure", limit=10) + +print("Recent Failures:") +for entry in failures: + print(f" [{entry['timestamp']}] {entry['skill']}") + print(f" Error: {entry['error_message']}") + print(f" Duration: {entry['duration_ms']}ms") + print() +``` + +## Error Handling + +### Invalid Status Value + +```python +# Raises ValueError +telemetry.capture( + skill="test.skill", + status="invalid_status", # Must be: success, failure, timeout, error, pending + duration_ms=100, + caller="CLI" +) +``` + +**Error:** `ValueError: Invalid status: invalid_status. Must be one of: success, failure, timeout, error, pending` + +### Malformed Input JSON (CLI) + +```bash +python skills/telemetry.capture/telemetry_capture.py \ + plugin.build success 320 CLI '{invalid json}' +``` + +**Error:** `Error: Invalid JSON for inputs: Expecting property name enclosed in double quotes` + +### File Locking Contention + +The implementation uses `fcntl.flock` for thread-safe writes. If multiple processes write simultaneously: +- Writes are serialized automatically +- No data loss occurs +- Performance may degrade under heavy contention + +## Dependencies + +This skill has no external dependencies beyond Python standard library: +- `json` - JSON parsing and serialization +- `fcntl` - File locking for thread safety +- `datetime` - ISO 8601 timestamp generation +- `pathlib` - Path handling +- `typing` - Type annotations + +## Configuration + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `BETTY_TELEMETRY_FILE` | `/home/user/betty/registry/telemetry.json` | Path to telemetry file | + +### Custom Telemetry File + +```python +from telemetry_capture import TelemetryCapture + +# Use custom location +telemetry = TelemetryCapture(telemetry_file="/custom/path/telemetry.json") +``` + +## Troubleshooting + +### Q: Telemetry file doesn't exist + +**A:** The skill automatically creates the telemetry file on first use. Ensure: +- The parent directory exists or can be created +- Write permissions are granted +- Path is absolute or correctly relative + +### Q: Decorator not capturing telemetry + +**A:** Ensure you: +1. Import the decorator correctly +2. Add the parent path to sys.path if needed +3. Check that the decorated function completes (doesn't hang) +4. Verify file permissions on `/registry/telemetry.json` + +### Q: How to exclude sensitive data? + +**A:** Use `sanitize_keys` parameter: + +```python +@capture_telemetry( + skill_name="auth.login", + capture_inputs=True, + sanitize_keys=["password", "api_key", "secret_token"] +) +def login(username: str, password: str): + # password will be redacted as "***REDACTED***" + pass +``` + +### Q: Performance impact of telemetry? + +**A:** Minimal impact: +- Decorator adds <1ms overhead per call +- File I/O is buffered and atomic +- No network calls +- Consider async writes for high-throughput scenarios + +## Integration with Betty Framework + +The `telemetry.capture` skill integrates with: + +- **agent.run**: Logs agent executions with task context +- **workflow.compose**: Traces multi-step workflow chains +- **plugin.build**: Monitors build performance +- **api.define**: Tracks API creation events +- **skill.define**: Captures skill registration +- **audit.log**: Complements audit trail with performance metrics + +All core Betty components should use the `@capture_telemetry` decorator for consistent observability. + +## License + +Part of the Betty Framework. See repository LICENSE. diff --git a/skills/telemetry.capture/__init__.py b/skills/telemetry.capture/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/telemetry.capture/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/telemetry.capture/skill.yaml b/skills/telemetry.capture/skill.yaml new file mode 100644 index 0000000..ae31296 --- /dev/null +++ b/skills/telemetry.capture/skill.yaml @@ -0,0 +1,97 @@ +name: telemetry.capture +version: 0.1.0 +description: > + Captures and logs usage telemetry for Betty Framework components. + Provides thread-safe JSON logging to /registry/telemetry.json with + ISO timestamps and structured metadata. + +inputs: + - name: skill + type: string + required: true + description: Name of the skill/component being logged (e.g., 'plugin.build', 'agent.run') + + - name: status + type: string + required: true + description: Execution status (success, failure, timeout, error, pending) + + - name: duration_ms + type: number + required: true + description: Execution duration in milliseconds + + - name: caller + type: string + required: true + description: Source of the call (e.g., CLI, API, workflow.compose) + + - name: inputs + type: object + required: false + default: {} + description: Sanitized input parameters (no secrets) + + - name: error_message + type: string + required: false + description: Error message if status is failure/error + + - name: metadata + type: object + required: false + default: {} + description: Additional context (user, session_id, environment, etc.) + +outputs: + - name: telemetry_entry + type: object + description: The captured telemetry entry with ISO timestamp + + - name: telemetry_file + type: string + description: Path to the telemetry.json file + +dependencies: [] + +status: active + +entrypoints: + - command: /telemetry/capture + handler: telemetry_capture.py + runtime: python + description: Capture telemetry event for a Betty component + parameters: + - name: skill + type: string + required: true + description: Skill name + - name: status + type: string + required: true + description: Execution status + - name: duration_ms + type: number + required: true + description: Duration in milliseconds + - name: caller + type: string + required: true + description: Caller identifier + - name: inputs + type: string + required: false + description: JSON string of inputs + - name: error_message + type: string + required: false + description: Error message + permissions: + - filesystem:read + - filesystem:write + +tags: + - telemetry + - logging + - observability + - audit diff --git a/skills/telemetry.capture/telemetry_capture.py b/skills/telemetry.capture/telemetry_capture.py new file mode 100755 index 0000000..b0b48ed --- /dev/null +++ b/skills/telemetry.capture/telemetry_capture.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Betty Framework - Telemetry Capture Skill +Logs usage of core Betty components to /registry/telemetry.json +""" + +import json +import os +import sys +import fcntl +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, Optional + + +class TelemetryCapture: + """Thread-safe telemetry capture for Betty Framework components.""" + + def __init__(self, telemetry_file: str = "/home/user/betty/registry/telemetry.json"): + self.telemetry_file = Path(telemetry_file) + self._ensure_telemetry_file() + + def _ensure_telemetry_file(self) -> None: + """Ensure telemetry file exists with valid JSON structure.""" + self.telemetry_file.parent.mkdir(parents=True, exist_ok=True) + + if not self.telemetry_file.exists(): + # Use simple list format to match existing telemetry.json + initial_data = [] + with open(self.telemetry_file, 'w') as f: + json.dump(initial_data, f, indent=2) + + def capture( + self, + skill: str, + status: str, + duration_ms: float, + caller: str, + inputs: Optional[Dict[str, Any]] = None, + error_message: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Capture telemetry event and append to telemetry.json (thread-safe). + + Args: + skill: Name of the skill/component (e.g., 'plugin.build', 'agent.run') + status: Execution status ('success', 'failure', 'timeout', 'error') + duration_ms: Execution duration in milliseconds + caller: Source of the call (e.g., 'CLI', 'API', 'workflow.compose') + inputs: Input parameters (sanitized, no secrets) + error_message: Error message if status is failure/error + metadata: Additional context (e.g., user, session_id, environment) + + Returns: + Dict containing the captured telemetry entry + """ + # Validate inputs + if status not in ['success', 'failure', 'timeout', 'error', 'pending']: + raise ValueError(f"Invalid status: {status}. Must be one of: success, failure, timeout, error, pending") + + # Build telemetry entry + entry = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "skill": skill, + "status": status, + "duration_ms": float(duration_ms), + "caller": caller, + "inputs": inputs or {}, + "error_message": error_message, + "metadata": metadata or {} + } + + # Thread-safe append to telemetry.json + self._append_entry(entry) + + return entry + + def _append_entry(self, entry: Dict[str, Any]) -> None: + """ + Append entry to telemetry file with file locking for thread safety. + + Uses fcntl.flock for POSIX systems to ensure atomic writes. + """ + with open(self.telemetry_file, 'r+') as f: + # Acquire exclusive lock + fcntl.flock(f.fileno(), fcntl.LOCK_EX) + + try: + # Read current data (simple list format) + f.seek(0) + data = json.load(f) + + # Ensure data is a list + if not isinstance(data, list): + data = [] + + # Append new entry + data.append(entry) + + # Keep last 100,000 entries (safety limit) + if len(data) > 100000: + data = data[-100000:] + + # Write back (truncate and rewrite) + f.seek(0) + f.truncate() + json.dump(data, f, indent=2) + f.write('\n') # Add newline at end + + finally: + # Release lock + fcntl.flock(f.fileno(), fcntl.LOCK_UN) + + def query( + self, + skill: Optional[str] = None, + status: Optional[str] = None, + caller: Optional[str] = None, + limit: Optional[int] = None + ) -> list: + """ + Query telemetry entries with optional filters. + + Args: + skill: Filter by skill name + status: Filter by status + caller: Filter by caller + limit: Maximum number of entries to return (most recent first) + + Returns: + List of matching telemetry entries + """ + with open(self.telemetry_file, 'r') as f: + data = json.load(f) + + # Data is a simple list + entries = data if isinstance(data, list) else [] + + # Apply filters + if skill: + entries = [e for e in entries if e.get("skill") == skill] + if status: + entries = [e for e in entries if e.get("status") == status] + if caller: + entries = [e for e in entries if e.get("caller") == caller] + + # Sort by timestamp (most recent first) and limit + entries = sorted(entries, key=lambda e: e.get("timestamp", ""), reverse=True) + + if limit: + entries = entries[:limit] + + return entries + + +def main(): + """CLI entrypoint for telemetry capture.""" + if len(sys.argv) < 5: + print("Usage: python telemetry_capture.py [inputs_json]") + print("\nExample:") + print(' python telemetry_capture.py plugin.build success 320 CLI') + print(' python telemetry_capture.py agent.run failure 1500 API \'{"agent": "api.designer"}\'') + sys.exit(1) + + skill = sys.argv[1] + status = sys.argv[2] + duration_ms = float(sys.argv[3]) + caller = sys.argv[4] + + # Parse inputs if provided + inputs = {} + if len(sys.argv) > 5: + try: + inputs = json.loads(sys.argv[5]) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON for inputs: {e}", file=sys.stderr) + sys.exit(1) + + # Parse error message if provided + error_message = None + if len(sys.argv) > 6: + error_message = sys.argv[6] + + # Capture telemetry + try: + telemetry = TelemetryCapture() + entry = telemetry.capture( + skill=skill, + status=status, + duration_ms=duration_ms, + caller=caller, + inputs=inputs, + error_message=error_message + ) + + print(json.dumps(entry, indent=2)) + print(f"\n✓ Telemetry captured to {telemetry.telemetry_file}") + + except Exception as e: + print(f"Error capturing telemetry: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/telemetry.capture/telemetry_utils.py b/skills/telemetry.capture/telemetry_utils.py new file mode 100755 index 0000000..b087d0a --- /dev/null +++ b/skills/telemetry.capture/telemetry_utils.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +Betty Framework - Telemetry Utilities +Decorator and utilities for automatic execution logging +""" + +import functools +import time +import inspect +from typing import Any, Callable, Dict, Optional +from pathlib import Path +from .telemetry_capture import TelemetryCapture + + +def capture_telemetry( + skill_name: Optional[str] = None, + caller: str = "runtime", + capture_inputs: bool = False, + sanitize_keys: Optional[list] = None +): + """ + Decorator to automatically capture telemetry for function execution. + + Usage: + @capture_telemetry(skill_name="agent.run", caller="CLI", capture_inputs=True) + def run_agent(agent_path, task_context=None): + # ... implementation + return result + + Args: + skill_name: Name of the skill/component. If None, uses function name. + caller: Source of the call (e.g., 'CLI', 'API', 'workflow') + capture_inputs: Whether to capture function arguments (default: False) + sanitize_keys: List of parameter names to exclude (e.g., ['password', 'api_key']) + + The decorated function can: + - Return a dict with 'status' key to set telemetry status + - Raise exceptions (captured as 'error' status) + - Return any value (captured as 'success' status) + """ + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Determine skill name + skill = skill_name or f"{func.__module__}.{func.__name__}" + + # Capture inputs if requested + inputs = {} + if capture_inputs: + # Get function signature + sig = inspect.signature(func) + bound_args = sig.bind_partial(*args, **kwargs) + bound_args.apply_defaults() + + # Sanitize inputs + inputs = dict(bound_args.arguments) + if sanitize_keys: + for key in sanitize_keys: + if key in inputs: + inputs[key] = "***REDACTED***" + + # Start timing + start_time = time.time() + status = "success" + error_message = None + result = None + + try: + # Execute function + result = func(*args, **kwargs) + + # Check if result contains status + if isinstance(result, dict) and 'status' in result: + status = result['status'] + + return result + + except Exception as e: + status = "error" + error_message = str(e) + raise # Re-raise the exception + + finally: + # Calculate duration + duration_ms = (time.time() - start_time) * 1000 + + # Capture telemetry + try: + telemetry = TelemetryCapture() + telemetry.capture( + skill=skill, + status=status, + duration_ms=duration_ms, + caller=caller, + inputs=inputs, + error_message=error_message + ) + except Exception as telemetry_error: + # Don't let telemetry errors break the actual function + print(f"Warning: Telemetry capture failed: {telemetry_error}", file=sys.stderr) + + return wrapper + return decorator + + +class TelemetryContext: + """ + Context manager for capturing telemetry around code blocks. + + Usage: + with TelemetryContext(skill="plugin.build", caller="CLI") as ctx: + # ... do work + ctx.set_inputs({"plugin_path": "./plugin.yaml"}) + result = build_plugin() + ctx.set_status("success") + """ + + def __init__( + self, + skill: str, + caller: str = "runtime", + inputs: Optional[Dict[str, Any]] = None + ): + self.skill = skill + self.caller = caller + self.inputs = inputs or {} + self.status = "success" + self.error_message = None + self.start_time = None + self.telemetry = TelemetryCapture() + + def __enter__(self): + self.start_time = time.time() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # Calculate duration + duration_ms = (time.time() - self.start_time) * 1000 + + # Set status based on exception + if exc_type is not None: + self.status = "error" + self.error_message = str(exc_val) + + # Capture telemetry + try: + self.telemetry.capture( + skill=self.skill, + status=self.status, + duration_ms=duration_ms, + caller=self.caller, + inputs=self.inputs, + error_message=self.error_message + ) + except Exception as telemetry_error: + print(f"Warning: Telemetry capture failed: {telemetry_error}", file=sys.stderr) + + # Don't suppress exceptions + return False + + def set_status(self, status: str): + """Update the execution status.""" + self.status = status + + def set_inputs(self, inputs: Dict[str, Any]): + """Update the captured inputs.""" + self.inputs.update(inputs) + + def set_error(self, error_message: str): + """Set error message.""" + self.error_message = error_message + self.status = "error" + + +# Example usage for testing +if __name__ == "__main__": + # Test decorator + @capture_telemetry(skill_name="test.function", caller="CLI", capture_inputs=True) + def test_function(name: str, value: int = 10): + """Test function for telemetry capture.""" + print(f"Executing test_function with name={name}, value={value}") + time.sleep(0.1) # Simulate work + return {"status": "success", "result": value * 2} + + # Test context manager + def test_context(): + """Test context manager for telemetry capture.""" + with TelemetryContext(skill="test.context", caller="CLI") as ctx: + ctx.set_inputs({"operation": "test"}) + print("Executing within context") + time.sleep(0.05) # Simulate work + ctx.set_status("success") + + print("Testing decorator...") + result = test_function("example", value=42) + print(f"Result: {result}\n") + + print("Testing context manager...") + test_context() + print("\nTelemetry tests complete!") diff --git a/skills/test.example/README.md b/skills/test.example/README.md new file mode 100644 index 0000000..8a8c0e9 --- /dev/null +++ b/skills/test.example/README.md @@ -0,0 +1,82 @@ +# test.example + +A simple test skill for validating the meta.create orchestrator workflow + +## Overview + +**Purpose:** A simple test skill for validating the meta.create orchestrator workflow + +**Command:** `/test/example` + +## Usage + +### Basic Usage + +```bash +python3 skills/test/example/test_example.py +``` + +### With Arguments + +```bash +python3 skills/test/example/test_example.py \ + --input_data_(string)_-_test_input_data "value" \ + --output-format json +``` + +## Inputs + +- **input_data (string) - Test input data** + +## Outputs + +- **output_result (string) - Processed result** + +## Artifact Metadata + +### Consumes + +- `test.input` + +### Produces + +- `test.result` + +## Examples + +- Process test data and produce test results +- Validate meta.create orchestration workflow + +## Permissions + +- `filesystem:read` + +## Implementation Notes + +This is a minimal test skill to verify that meta.create can properly orchestrate the creation of skills, check for duplicates, and validate compatibility. + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - test.example +``` + +## Testing + +Run tests with: + +```bash +pytest skills/test/example/test_test_example.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/test.example/__init__.py b/skills/test.example/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/test.example/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/test.example/skill.yaml b/skills/test.example/skill.yaml new file mode 100644 index 0000000..f5b627f --- /dev/null +++ b/skills/test.example/skill.yaml @@ -0,0 +1,21 @@ +name: test.example +version: 0.1.0 +description: A simple test skill for validating the meta.create orchestrator workflow +inputs: +- input_data (string) - Test input data +outputs: +- output_result (string) - Processed result +status: active +permissions: +- filesystem:read +entrypoints: +- command: /test/example + handler: test_example.py + runtime: python + description: A simple test skill for validating the meta.create orchestrator workflow +artifact_metadata: + produces: + - type: test.result + consumes: + - type: test.input + required: true diff --git a/skills/test.example/test_example.py b/skills/test.example/test_example.py new file mode 100755 index 0000000..589bc96 --- /dev/null +++ b/skills/test.example/test_example.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +test.example - A simple test skill for validating the meta.create orchestrator workflow + +Generated by meta.skill with Betty Framework certification +""" + +import os +import sys +import json +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional + + +from betty.config import BASE_DIR +from betty.logging_utils import setup_logger +from betty.certification import certified_skill + +logger = setup_logger(__name__) + + +class TestExample: + """ + A simple test skill for validating the meta.create orchestrator workflow + """ + + def __init__(self, base_dir: str = BASE_DIR): + """Initialize skill""" + self.base_dir = Path(base_dir) + + @certified_skill("test.example") + def execute(self, input_data_string___test_input_data: Optional[str] = None) -> Dict[str, Any]: + """ + Execute the skill + + Returns: + Dict with execution results + """ + try: + logger.info("Executing test.example...") + + # TODO: Implement skill logic here + + # Implementation notes: + # This is a minimal test skill to verify that meta.create can properly orchestrate the creation of skills, check for duplicates, and validate compatibility. + + # Placeholder implementation + result = { + "ok": True, + "status": "success", + "message": "Skill executed successfully" + } + + logger.info("Skill completed successfully") + return result + + except Exception as e: + logger.error(f"Error executing skill: {e}") + return { + "ok": False, + "status": "failed", + "error": str(e) + } + + +def main(): + """CLI entry point""" + import argparse + + parser = argparse.ArgumentParser( + description="A simple test skill for validating the meta.create orchestrator workflow" + ) + + parser.add_argument( + "--input-data-string---test-input-data", + help="input_data (string) - Test input data" + ) + parser.add_argument( + "--output-format", + choices=["json", "yaml"], + default="json", + help="Output format" + ) + + args = parser.parse_args() + + # Create skill instance + skill = TestExample() + + # Execute skill + result = skill.execute( + input_data_string___test_input_data=args.input_data_string___test_input_data, + ) + + # Output result + if args.output_format == "json": + print(json.dumps(result, indent=2)) + else: + print(yaml.dump(result, default_flow_style=False)) + + # Exit with appropriate code + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() diff --git a/skills/test.example/test_test_example.py b/skills/test.example/test_test_example.py new file mode 100644 index 0000000..e776421 --- /dev/null +++ b/skills/test.example/test_test_example.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for test.example + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.test_example import test_example + + +class TestTestExample: + """Tests for TestExample""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = test_example.TestExample() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["test_example.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + test_example.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "A simple test skill for validating the meta.create" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/threat.model.generate/SKILL.md b/skills/threat.model.generate/SKILL.md new file mode 100644 index 0000000..433afb6 --- /dev/null +++ b/skills/threat.model.generate/SKILL.md @@ -0,0 +1,28 @@ +--- +name: threat.model.generate +description: Generate STRIDE-based threat models with intelligent threat analysis, CVSS risk scoring, and mitigation recommendations +--- + +# threat.model.generate + +Generate STRIDE-based threat models with intelligent threat analysis, CVSS risk scoring, and mitigation recommendations + +## Status + +Auto-generated via `skill.create`. + +## Usage + +TODO: Add usage instructions + +## Inputs + +TODO: Document inputs + +## Outputs + +TODO: Document outputs + +## Dependencies + +TODO: List dependencies diff --git a/skills/threat.model.generate/skill.yaml b/skills/threat.model.generate/skill.yaml new file mode 100644 index 0000000..2473886 --- /dev/null +++ b/skills/threat.model.generate/skill.yaml @@ -0,0 +1,152 @@ +name: threat.model.generate +version: 0.1.0 +description: > + Generate STRIDE-based threat models with intelligent threat analysis, CVSS risk scoring, + and mitigation recommendations using Microsoft threat modeling methodology. Provides + specialized security expertise beyond simple template filling. + +inputs: + - name: system_description + type: string + required: true + description: Detailed description of system architecture, components, and functionality + + - name: data_flows + type: object + required: false + description: Data flows between components (auto-detected if not provided) + + - name: trust_boundaries + type: array + required: false + description: Trust boundaries in the system (auto-detected if not provided) + + - name: assets + type: array + required: false + description: Critical assets to protect (auto-detected if not provided) + + - name: frameworks + type: array + required: false + default: ["STRIDE"] + description: Threat modeling frameworks to apply (STRIDE, PASTA, LINDDUN) + + - name: risk_tolerance + type: string + required: false + default: "medium" + description: Organization risk tolerance (low, medium, high) + + - name: output_path + type: string + required: false + default: "./threat-model.yaml" + description: Path where threat model should be saved + +outputs: + - name: threat_model + type: object + description: Complete threat model with threats, risks, and mitigations + + - name: threat_model_file + type: string + description: Path to generated threat model YAML file + + - name: threat_count + type: number + description: Total number of threats identified + + - name: high_risk_count + type: number + description: Number of high-risk threats (CVSS >= 7.0) + + - name: coverage_report + type: object + description: STRIDE coverage analysis showing threat categories analyzed + +dependencies: + - PyYAML + - jsonschema + +status: draft + +tags: + - security + - threat-modeling + - stride + - risk-assessment + - cvss + - specialized + +artifact_metadata: + produces: + - type: threat-model + description: STRIDE-based threat model with attack vectors, risk scoring (CVSS), and security controls + file_pattern: "*.threat-model.yaml" + content_type: application/yaml + schema: schemas/artifacts/threat-model-schema.json + + consumes: + - type: architecture-overview + description: System architecture description (optional, enriches threat model) + file_pattern: "*.architecture-overview.md" + content_type: text/markdown + + - type: data-flow-diagrams + description: Data flows to identify threat vectors (optional) + file_pattern: "*.data-flow-diagrams.*" + content_type: "" + + - type: logical-data-model + description: Data structures and sensitive data to protect (optional) + file_pattern: "*.logical-data-model.*" + content_type: "" + +entrypoints: + - command: /skill/threat/model/generate + handler: threat_model_generate.py + runtime: python + description: > + Generate STRIDE-based threat models with intelligent threat analysis. + Applies Microsoft threat modeling methodology to identify security threats, + calculate CVSS risk scores, and recommend mitigations. + parameters: + - name: system_description + type: string + required: true + description: System description for threat modeling + + - name: data_flows + type: object + required: false + description: Data flows between components + + - name: trust_boundaries + type: array + required: false + description: Trust boundaries + + - name: assets + type: array + required: false + description: Critical assets + + - name: frameworks + type: array + required: false + description: Threat frameworks to apply + + - name: risk_tolerance + type: string + required: false + description: Risk tolerance level + + - name: output_path + type: string + required: false + description: Output file path + + permissions: + - filesystem:read + - filesystem:write diff --git a/skills/threat.model.generate/threat_model_generate.py b/skills/threat.model.generate/threat_model_generate.py new file mode 100755 index 0000000..67102fa --- /dev/null +++ b/skills/threat.model.generate/threat_model_generate.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +""" +threat.model.generate - Implementation Script +Auto-generated by skill.create +""" + +import os +import sys +import json +import argparse + +# Add Betty framework to path + +from betty.logging_utils import setup_logger +from betty.errors import format_error_response + +logger = setup_logger(__name__) + + +def main(): + """Main entry point for threat.model.generate.""" + parser = argparse.ArgumentParser(description="threat.model.generate") + # TODO: Add arguments + args = parser.parse_args() + + try: + logger.info("Executing threat.model.generate...") + # TODO: Implement skill logic + result = {"status": "success", "message": "Not yet implemented"} + print(json.dumps(result, indent=2)) + except Exception as e: + logger.error(f"Error executing threat.model.generate: {e}") + print(json.dumps(format_error_response(e), indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/workflow.compose/SKILL.md b/skills/workflow.compose/SKILL.md new file mode 100644 index 0000000..5470295 --- /dev/null +++ b/skills/workflow.compose/SKILL.md @@ -0,0 +1,309 @@ +--- +name: Workflow Compose +description: Executes multi-step workflows by chaining Betty Framework skills. +--- + +# workflow.compose + +## Purpose + +Allows declarative execution of Betty Framework workflows by reading a YAML definition and chaining skills like `skill.create`, `skill.define`, and `registry.update`. + +Enables complex multi-step processes to be defined once and executed reliably with proper error handling and audit logging. + +## Usage + +### Basic Usage + +```bash +python skills/workflow.compose/workflow_compose.py +``` + +### Arguments + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| workflow_path | string | Yes | Path to the workflow YAML file to execute | + +## Workflow YAML Structure + +```yaml +# workflows/create_and_register.yaml +name: "Create and Register Skill" +description: "Complete lifecycle: create, validate, and register a new skill" + +steps: + - skill: skill.create + args: ["workflow.validate", "Validates workflow definitions"] + required: true + + - skill: skill.define + args: ["skills/workflow.validate/skill.yaml"] + required: true + + - skill: registry.update + args: ["skills/workflow.validate/skill.yaml"] + required: false # Continue even if this fails +``` + +### Workflow Fields + +| Field | Required | Description | Example | +|-------|----------|-------------|---------| +| `name` | No | Workflow name | `"API Design Workflow"` | +| `description` | No | What the workflow does | `"Complete API lifecycle"` | +| `steps` | Yes | Array of steps to execute | See below | + +### Step Fields + +| Field | Required | Description | Example | +|-------|----------|-------------|---------| +| `skill` | Yes | Skill name to execute | `api.validate` | +| `args` | No | Arguments to pass to skill | `["specs/api.yaml", "zalando"]` | +| `required` | No | Stop workflow if step fails | `true` (default: `false`) | + +## Behavior + +1. **Load Workflow**: Parses the workflow YAML file +2. **Sequential Execution**: Runs each step in order +3. **Error Handling**: + - If `required: true`, workflow stops on failure + - If `required: false`, workflow continues and logs error +4. **Audit Logging**: Calls `audit.log` skill (if available) for each step +5. **History Tracking**: Records execution history in `/registry/workflow_history.json` + +## Outputs + +### Success Response + +```json +{ + "ok": true, + "status": "success", + "errors": [], + "path": "workflows/create_and_register.yaml", + "details": { + "workflow_name": "Create and Register Skill", + "steps_executed": 3, + "steps_succeeded": 3, + "steps_failed": 0, + "duration_ms": 1234, + "history_file": "/registry/workflow_history.json" + } +} +``` + +### Partial Failure Response + +```json +{ + "ok": false, + "status": "failed", + "errors": [ + "Step 2 (skill.define) failed: Missing required fields: version" + ], + "path": "workflows/create_and_register.yaml", + "details": { + "workflow_name": "Create and Register Skill", + "steps_executed": 2, + "steps_succeeded": 1, + "steps_failed": 1, + "failed_step": "skill.define", + "failed_step_index": 1 + } +} +``` + +## Example Workflow Files + +### Example 1: Complete Skill Lifecycle + +```yaml +# workflows/create_and_register.yaml +name: "Create and Register Skill" +description: "Scaffold, validate, and register a new skill" + +steps: + - skill: skill.create + args: ["workflow.validate", "Validates workflow definitions"] + required: true + + - skill: skill.define + args: ["skills/workflow.validate/skill.yaml"] + required: true + + - skill: registry.update + args: ["skills/workflow.validate/skill.yaml"] + required: true +``` + +**Execution**: + +```bash +$ python skills/workflow.compose/workflow_compose.py workflows/create_and_register.yaml +{ + "ok": true, + "status": "success", + "details": { + "steps_executed": 3, + "steps_succeeded": 3 + } +} +``` + +### Example 2: API Design Workflow + +```yaml +# workflows/api_design.yaml +name: "API Design Workflow" +description: "Design, validate, and generate models for new API" + +steps: + - skill: api.define + args: ["user-service", "openapi", "zalando", "specs", "1.0.0"] + required: true + + - skill: api.validate + args: ["specs/user-service.openapi.yaml", "zalando", "true"] + required: true + + - skill: api.generate-models + args: ["specs/user-service.openapi.yaml", "typescript", "src/models"] + required: false # Continue even if model generation fails +``` + +### Example 3: Multi-Spec Validation + +```yaml +# workflows/validate_all_specs.yaml +name: "Validate All API Specs" +description: "Validate all OpenAPI specifications in specs directory" + +steps: + - skill: api.validate + args: ["specs/users.openapi.yaml", "zalando"] + required: false + + - skill: api.validate + args: ["specs/orders.openapi.yaml", "zalando"] + required: false + + - skill: api.validate + args: ["specs/payments.openapi.yaml", "zalando"] + required: false +``` + +## Workflow History + +Execution history is logged to `/registry/workflow_history.json`: + +```json +{ + "executions": [ + { + "workflow_path": "workflows/create_and_register.yaml", + "workflow_name": "Create and Register Skill", + "timestamp": "2025-10-23T12:34:56Z", + "status": "success", + "steps_executed": 3, + "steps_succeeded": 3, + "duration_ms": 1234 + } + ] +} +``` + +## Audit Integration + +If `audit.log` skill is available, each step execution is logged: + +```python +log_audit_entry( + skill_name="api.validate", + status="success", + duration_ms=456, + metadata={"workflow": "api_design.yaml", "step": 1} +) +``` + +## Integration + +### With workflow.validate + +Validate workflow syntax before execution: + +```bash +# Validate first +python skills/workflow.validate/workflow_validate.py workflows/my-workflow.yaml + +# Then execute +python skills/workflow.compose/workflow_compose.py workflows/my-workflow.yaml +``` + +### With Hooks + +Auto-validate workflows when saved: + +```bash +python skills/hook.define/hook_define.py \ + --event on_file_save \ + --pattern "workflows/*.yaml" \ + --command "python skills/workflow.validate/workflow_validate.py {file_path}" \ + --blocking true +``` + +### In CI/CD + +```yaml +# .github/workflows/test.yml +- name: Run workflow tests + run: | + python skills/workflow.compose/workflow_compose.py workflows/test_suite.yaml +``` + +## Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "Workflow file not found" | Path incorrect | Check workflow file path | +| "Invalid YAML in workflow" | Malformed YAML | Fix YAML syntax errors | +| "Skill handler not found" | Referenced skill doesn't exist | Ensure skill is registered or path is correct | +| "Step X failed" | Skill execution failed | Check skill's error output, fix issues | +| "Skill execution timed out" | Skill took >5 minutes | Optimize skill or increase timeout in code | + +## Best Practices + +1. **Validate First**: Run `workflow.validate` before executing workflows +2. **Use Required Judiciously**: Only mark critical steps as `required: true` +3. **Small Workflows**: Keep workflows focused on single logical task +4. **Error Handling**: Plan for partial failures in non-required steps +5. **Test Workflows**: Test workflows in development before using in production +6. **Version Control**: Keep workflow files in git + +## Files Modified + +- **History**: `/registry/workflow_history.json` – Execution history +- **Logs**: Step execution logged to Betty's logging system + +## Exit Codes + +- **0**: Success (all required steps succeeded) +- **1**: Failure (at least one required step failed) + +## Timeout + +Each skill has a 5-minute (300 second) timeout by default. If a skill exceeds this, the workflow fails. + +## See Also + +- **workflow.validate** – Validate workflow syntax ([workflow.validate SKILL.md](../workflow.validate/SKILL.md)) +- **Betty Architecture** – [Five-Layer Model](../../docs/betty-architecture.md) for understanding workflows +- **API-Driven Development** – [Example workflows](../../docs/api-driven-development.md) + +## Status + +**Active** – Production-ready, core orchestration skill + +## Version History + +- **0.1.0** (Oct 2025) – Initial implementation with sequential execution, error handling, and audit logging diff --git a/skills/workflow.compose/__init__.py b/skills/workflow.compose/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/workflow.compose/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/workflow.compose/skill.yaml b/skills/workflow.compose/skill.yaml new file mode 100644 index 0000000..d5dfdf6 --- /dev/null +++ b/skills/workflow.compose/skill.yaml @@ -0,0 +1,30 @@ +name: workflow.compose +version: 0.1.0 +description: > + Executes multi-step Betty Framework workflows by chaining existing skills. + Enables declarative orchestration of skill pipelines. +inputs: + - workflow_path +outputs: + - workflow_history.json +dependencies: + - skill.create + - skill.define + - registry.update +status: active + +entrypoints: + - command: /workflow/compose + handler: workflow_compose.py + runtime: python + description: > + Execute a Betty workflow defined in a YAML file. + parameters: + - name: workflow_path + type: string + required: true + description: Path to a workflow YAML file to execute. + permissions: + - filesystem + - read + - write diff --git a/skills/workflow.compose/workflow_compose.py b/skills/workflow.compose/workflow_compose.py new file mode 100644 index 0000000..264ac75 --- /dev/null +++ b/skills/workflow.compose/workflow_compose.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +""" +workflow_compose.py – Implementation of the workflow.compose Skill +Executes multi-step Betty Framework workflows by chaining existing skills. +""" +import os +import sys +import yaml +import json +from typing import Dict, Any, List, Optional, Tuple +from datetime import datetime, timezone +from pydantic import ValidationError as PydanticValidationError +from betty.config import BASE_DIR, WORKFLOW_HISTORY_FILE, get_skill_handler_path +from betty.file_utils import safe_update_json +from betty.validation import validate_path +from betty.logging_utils import setup_logger +from betty.errors import WorkflowError, format_error_response +from betty.telemetry_capture import capture_skill_execution +from betty.models import WorkflowDefinition +from betty.skill_executor import execute_skill_in_process +from betty.provenance import compute_hash, get_provenance_logger +from utils.telemetry_utils import capture_telemetry +logger = setup_logger(__name__) + + +def log_audit_entry( + skill_name: str, + status: str, + duration_ms: Optional[int] = None, + errors: Optional[List[str]] = None, + metadata: Optional[Dict[str, Any]] = None, +) -> None: + """ + Log an audit entry for skill execution. + + Args: + skill_name: Name of the skill + status: Execution status (success, failed, etc.) + duration_ms: Execution duration in milliseconds + errors: List of errors (if any) + metadata: Additional metadata + """ + try: + args = [skill_name, status] + + if duration_ms is not None: + args.append(str(duration_ms)) + else: + args.append("") + + if errors: + args.append(json.dumps(errors)) + else: + args.append("[]") + + if metadata: + args.append(json.dumps(metadata)) + + result = execute_skill_in_process("audit.log", args, timeout=10) + + if result["returncode"] != 0: + logger.warning(f"Failed to log audit entry for {skill_name}: {result['stderr']}") + except Exception as e: + logger.warning(f"Failed to log audit entry for {skill_name}: {e}") +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_workflow(workflow_file: str) -> Dict[str, Any]: + """ + Load and parse a workflow YAML file. + Args: + workflow_file: Path to workflow YAML file + Returns: + Parsed workflow dictionary + Raises: + WorkflowError: If workflow cannot be loaded + """ + try: + with open(workflow_file) as f: + workflow = yaml.safe_load(f) + + # Validate with Pydantic schema + try: + WorkflowDefinition.model_validate(workflow) + logger.info("Pydantic schema validation passed for workflow") + except PydanticValidationError as exc: + errors = [] + for error in exc.errors(): + field = ".".join(str(loc) for loc in error["loc"]) + message = error["msg"] + errors.append(f"{field}: {message}") + raise WorkflowError(f"Workflow schema validation failed: {'; '.join(errors)}") + + return workflow + except FileNotFoundError: + raise WorkflowError(f"Workflow file not found: {workflow_file}") + except yaml.YAMLError as e: + raise WorkflowError(f"Invalid YAML in workflow: {e}") +def run_skill(skill_name: str, args: List[str]) -> Dict[str, Any]: + """ + Run a skill handler in-process using dynamic imports. + + Args: + skill_name: Name of the skill (e.g., "workflow.validate", "audit.log") + args: Arguments to pass to the skill + + Returns: + Dictionary with stdout, stderr, and return code + + Raises: + WorkflowError: If skill execution fails + """ + logger.info(f"▶ Running skill {skill_name} with args: {' '.join(args)}") + + # Verify skill handler exists + skill_path = get_skill_handler_path(skill_name) + if not os.path.exists(skill_path): + raise WorkflowError(f"Skill handler not found: {skill_path}") + + try: + # Execute skill in-process with 5 minute timeout + result = execute_skill_in_process(skill_name, args, timeout=300) + return result + except Exception as e: + raise WorkflowError(f"Failed to execute skill: {e}") +def save_workflow_history(log: Dict[str, Any]) -> None: + """ + Save workflow execution history with content hashing for provenance. + Args: + log: Workflow execution log + """ + try: + # Compute content hash for provenance tracking + content_hash = compute_hash(log) + log["content_hash"] = content_hash + + # Log to provenance system + provenance = get_provenance_logger() + workflow_name = log.get("workflow", "unknown") + artifact_id = f"workflow.execution.{workflow_name}" + + provenance.log_artifact( + artifact_id=artifact_id, + version=log.get("started_at", "unknown"), + content_hash=content_hash, + artifact_type="workflow-execution", + metadata={ + "workflow": workflow_name, + "status": log.get("status", "unknown"), + "total_steps": len(log.get("steps", [])), + } + ) + + # Save to history file + def update_fn(history_data): + """Update function for safe_update_json.""" + if not isinstance(history_data, list): + history_data = [] + history_data.append(log) + # Keep only last 100 workflow runs + return history_data[-100:] + + safe_update_json(WORKFLOW_HISTORY_FILE, update_fn, default=[]) + logger.info(f"Workflow history saved to {WORKFLOW_HISTORY_FILE} with hash {content_hash[:8]}...") + except Exception as e: + logger.warning(f"Failed to save workflow history: {e}") +def execute_workflow(workflow_file: str) -> Dict[str, Any]: + """Read a workflow YAML and execute skills sequentially.""" + validate_path(workflow_file, must_exist=True) + workflow = load_workflow(workflow_file) + fail_fast = workflow.get("fail_fast", True) + log: Dict[str, Any] = { + "workflow": os.path.basename(workflow_file), + "workflow_path": workflow_file, + "started_at": datetime.now(timezone.utc).isoformat(), + "fail_fast": fail_fast, + "steps": [], + "status": "running", + } + aggregated_errors: List[str] = [] + # Validate workflow definition before executing steps + validation_result = run_skill("workflow.validate", [workflow_file]) + validation_log: Dict[str, Any] = { + "step": "validation", + "skill": "workflow.validate", + "args": [workflow_file], + "returncode": validation_result["returncode"], + "stdout": validation_result["stdout"], + "stderr": validation_result["stderr"], + "parsed": validation_result.get("parsed"), + "parse_error": validation_result.get("parse_error"), + "status": "success" if validation_result["returncode"] == 0 else "failed", + "errors": [], + } + parsed_validation = validation_result.get("parsed") + if isinstance(parsed_validation, dict) and parsed_validation.get("errors"): + validation_log["errors"] = [str(err) for err in parsed_validation.get("errors", [])] + if validation_result.get("parse_error") and not validation_log["errors"]: + validation_log["errors"] = [validation_result["parse_error"]] + log["validation"] = validation_log + if validation_log["status"] != "success" or ( + isinstance(parsed_validation, dict) and not parsed_validation.get("ok", True) + ): + if validation_log["errors"]: + aggregated_errors.extend(validation_log["errors"]) + else: + aggregated_errors.append( + f"workflow.validate failed with return code {validation_result['returncode']}" + ) + log["status"] = "failed" + log["errors"] = aggregated_errors + log["completed_at"] = datetime.now(timezone.utc).isoformat() + save_workflow_history(log) + return log + steps = workflow.get("steps", []) + if not isinstance(steps, list) or not steps: + raise WorkflowError("Workflow has no steps defined") + logger.info(f"Executing workflow: {workflow_file}") + logger.info(f"Total steps: {len(steps)}") + failed_steps: List[Dict[str, Any]] = [] + for i, step in enumerate(steps, 1): + # Support both 'skill' and 'agent' step types + is_agent_step = "agent" in step + is_skill_step = "skill" in step + + step_log: Dict[str, Any] = { + "step_number": i, + "skill": step.get("skill") if is_skill_step else None, + "agent": step.get("agent") if is_agent_step else None, + "args": step.get("args", []), + "status": "pending", + } + + # Validate step has either skill or agent field + if not is_skill_step and not is_agent_step: + error = f"Step {i} missing required 'skill' or 'agent' field" + logger.error(error) + step_log["status"] = "failed" + step_log["errors"] = [error] + aggregated_errors.append(error) + failed_steps.append({"step": i, "error": error}) + log["steps"].append(step_log) + if fail_fast: + break + continue + + if is_skill_step and is_agent_step: + error = f"Step {i} cannot have both 'skill' and 'agent' fields" + logger.error(error) + step_log["status"] = "failed" + step_log["errors"] = [error] + aggregated_errors.append(error) + failed_steps.append({"step": i, "error": error}) + log["steps"].append(step_log) + if fail_fast: + break + continue + + # Handle agent steps by delegating to run.agent skill + if is_agent_step: + agent_name = step["agent"] + input_text = step.get("input", "") + skill_name = "run.agent" + args = [agent_name] + if input_text: + args.append(input_text) + logger.info(f"\n=== Step {i}/{len(steps)}: Executing agent {agent_name} via run.agent ===") + else: + skill_name = step["skill"] + args = step.get("args", []) + logger.info(f"\n=== Step {i}/{len(steps)}: Executing {skill_name} ===") + try: + step_start_time = datetime.now(timezone.utc) + execution_result = run_skill(skill_name, args) + step_end_time = datetime.now(timezone.utc) + step_duration_ms = int((step_end_time - step_start_time).total_seconds() * 1000) + + parsed_step = execution_result.get("parsed") + step_errors: List[str] = [] + if isinstance(parsed_step, dict) and parsed_step.get("errors"): + step_errors = [str(err) for err in parsed_step.get("errors", [])] + elif execution_result["returncode"] != 0: + step_errors = [ + f"Step {i} failed with return code {execution_result['returncode']}" + ] + step_log.update( + { + "skill": skill_name, + "args": args, + "returncode": execution_result["returncode"], + "stdout": execution_result["stdout"], + "stderr": execution_result["stderr"], + "parsed": parsed_step, + "parse_error": execution_result.get("parse_error"), + "status": "success" if execution_result["returncode"] == 0 else "failed", + "errors": step_errors, + "duration_ms": step_duration_ms, + } + ) + log["steps"].append(step_log) + + # Log audit entry for this step + log_audit_entry( + skill_name=skill_name, + status="success" if execution_result["returncode"] == 0 else "failed", + duration_ms=step_duration_ms, + errors=step_errors if step_errors else None, + metadata={ + "workflow": os.path.basename(workflow_file), + "step_number": i, + "total_steps": len(steps), + } + ) + + # Capture telemetry for this step + capture_skill_execution( + skill_name=skill_name, + inputs={"args": args}, + status="success" if execution_result["returncode"] == 0 else "failed", + duration_ms=step_duration_ms, + workflow=os.path.basename(workflow_file), + caller="workflow.compose", + error=step_errors[0] if step_errors else None, + step_number=i, + total_steps=len(steps), + ) + if execution_result["returncode"] != 0: + failed_steps.append({ + "step": i, + "skill": skill_name, + "returncode": execution_result["returncode"], + "errors": step_errors, + }) + aggregated_errors.extend(step_errors or [ + f"Step {i} ({skill_name}) failed with return code {execution_result['returncode']}" + ]) + logger.error( + f"❌ Step {i} failed with return code {execution_result['returncode']}" + ) + if fail_fast: + logger.error("Stopping workflow due to failure (fail_fast=true)") + break + else: + logger.info(f"✅ Step {i} completed successfully") + except WorkflowError as e: + error_msg = str(e) + logger.error(f"❌ Step {i} failed: {error_msg}") + step_log.update( + { + "returncode": None, + "stdout": "", + "stderr": "", + "parsed": None, + "parse_error": None, + "status": "failed", + "errors": [error_msg], + } + ) + aggregated_errors.append(error_msg) + failed_steps.append({"step": i, "skill": skill_name, "error": error_msg}) + log["steps"].append(step_log) + + # Log audit entry for failed step + log_audit_entry( + skill_name=skill_name, + status="failed", + errors=[error_msg], + metadata={ + "workflow": os.path.basename(workflow_file), + "step_number": i, + "total_steps": len(steps), + "error_type": "WorkflowError", + } + ) + + # Capture telemetry for failed step + capture_skill_execution( + skill_name=skill_name, + inputs={"args": args}, + status="failed", + duration_ms=0, # No duration available for exception cases + workflow=os.path.basename(workflow_file), + caller="workflow.compose", + error=error_msg, + step_number=i, + total_steps=len(steps), + error_type="WorkflowError", + ) + + if fail_fast: + break + if failed_steps: + log["status"] = "failed" + log["failed_steps"] = failed_steps + else: + log["status"] = "success" + log["errors"] = aggregated_errors + log["completed_at"] = datetime.now(timezone.utc).isoformat() + save_workflow_history(log) + + # Calculate total workflow duration + workflow_duration_ms = None + if "started_at" in log and "completed_at" in log: + start = datetime.fromisoformat(log["started_at"]) + end = datetime.fromisoformat(log["completed_at"]) + workflow_duration_ms = int((end - start).total_seconds() * 1000) + + # Log audit entry for overall workflow + log_audit_entry( + skill_name="workflow.compose", + status=log["status"], + duration_ms=workflow_duration_ms, + errors=aggregated_errors if aggregated_errors else None, + metadata={ + "workflow": os.path.basename(workflow_file), + "total_steps": len(steps), + "failed_steps": len(failed_steps), + } + ) + + # Capture telemetry for overall workflow + capture_skill_execution( + skill_name="workflow.compose", + inputs={"workflow_file": workflow_file}, + status=log["status"], + duration_ms=workflow_duration_ms or 0, + caller="cli", + error=aggregated_errors[0] if aggregated_errors else None, + workflow=os.path.basename(workflow_file), + total_steps=len(steps), + failed_steps=len(failed_steps), + completed_steps=len(steps) - len(failed_steps), + ) + + if log["status"] == "success": + logger.info("\n✅ Workflow completed successfully") + else: + logger.error(f"\n❌ Workflow completed with {len(failed_steps)} failed step(s)") + return log + + +@capture_telemetry(skill_name="workflow.compose", caller="cli") +def main(): + """Main CLI entry point.""" + if len(sys.argv) < 2: + message = "Usage: workflow_compose.py " + response = build_response( + False, + path="", + errors=[message], + details={"error": {"error": "UsageError", "message": message, "details": {}}}, + ) + print(json.dumps(response, indent=2)) + sys.exit(1) + workflow_file = sys.argv[1] + try: + details = execute_workflow(workflow_file) + response = build_response( + details.get("status") == "success", + path=WORKFLOW_HISTORY_FILE, + errors=details.get("errors", []), + details=details, + ) + print(json.dumps(response, indent=2)) + sys.exit(0 if response["ok"] else 1) + except WorkflowError as e: + logger.error(str(e)) + error_info = format_error_response(e) + response = build_response( + False, + path=workflow_file, + 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=workflow_file, + errors=[error_info.get("message", str(e))], + details={"error": error_info}, + ) + print(json.dumps(response, indent=2)) + sys.exit(1) +if __name__ == "__main__": + main() diff --git a/skills/workflow.orchestrate/README.md b/skills/workflow.orchestrate/README.md new file mode 100644 index 0000000..76e5141 --- /dev/null +++ b/skills/workflow.orchestrate/README.md @@ -0,0 +1,106 @@ +# workflow.orchestrate + +Orchestrate multi-artifact workflows by coordinating specialized agents and artifact creation skills. Creates complete artifact sets for complex initiatives by managing dependencies, sequencing work, and ensuring artifact consistency. Supports common SDLC workflows like project initiation, security reviews, data design, test planning, deployment planning, and full SDLC cycles. + +## Overview + +**Purpose:** Orchestrate multi-artifact workflows by coordinating specialized agents and artifact creation skills. Creates complete artifact sets for complex initiatives by managing dependencies, sequencing work, and ensuring artifact consistency. Supports common SDLC workflows like project initiation, security reviews, data design, test planning, deployment planning, and full SDLC cycles. + +**Command:** `/workflow/orchestrate` + +## Usage + +### Basic Usage + +```bash +python3 skills/workflow/orchestrate/workflow_orchestrate.py +``` + +### With Arguments + +```bash +python3 skills/workflow/orchestrate/workflow_orchestrate.py \ + --workflow_type_(string,_required):_type_of_workflow_to_execute_(project-initiation,_security-review,_data-design,_test-planning,_deployment-planning,_full-sdlc) "value" \ + --description_(string,_required):_initiative_or_project_description_for_context "value" \ + --output_directory_(string,_required):_directory_where_all_workflow_artifacts_will_be_created "value" \ + --author_(string,_optional):_author_name_for_generated_artifacts_(default:_"workflow_orchestrator") "value" \ + --classification_(string,_optional):_classification_level_(public,_internal,_confidential,_restricted,_default:_internal) "value" \ + --output-format json +``` + +## Inputs + +- **workflow_type (string, required): Type of workflow to execute (project-initiation, security-review, data-design, test-planning, deployment-planning, full-sdlc)** +- **description (string, required): Initiative or project description for context** +- **output_directory (string, required): Directory where all workflow artifacts will be created** +- **author (string, optional): Author name for generated artifacts (default: "Workflow Orchestrator")** +- **classification (string, optional): Classification level (Public, Internal, Confidential, Restricted, default: Internal)** + +## Outputs + +- **workflow-execution-report: Complete workflow execution report with all generated artifacts** +- **artifact-manifest: Manifest of all created artifacts with paths and validation status** +- **All artifacts specified by the workflow type (e.g., business-case, project-charter, etc.)** +- **None (initiates workflows from user context)** +- **workflow-execution-report** +- **artifact-manifest** +- **Plus all artifacts specified by the chosen workflow type** +- **filesystem:read** +- **filesystem:write** +- **artifact.create skill (to create individual artifacts)** +- **artifact.validate skill (to validate created artifacts)** +- **Template files in templates/ directory** +- **Call artifact.create with appropriate type and context** +- **Call artifact.validate to check quality** +- **Track created artifacts and validation results** +- **Handle missing dependencies gracefully** +- **Continue workflow even if individual artifacts fail** +- **Report all failures in execution report** +- **Return overall success based on whether any artifacts were created** + +## Examples + +- -author "Product Team" +- business-case.yaml +- project-charter.yaml +- raid-log.yaml +- stakeholder-analysis.yaml +- project-initiation-workflow-report.yaml +- -classification Confidential +- threat-model.yaml +- security-architecture-diagram.yaml +- security-assessment.yaml +- vulnerability-management-plan.yaml +- security-review-workflow-report.yaml +- -author "Enterprise Architecture" +- workflow_type: One of the supported workflow types +- description: Project/initiative description +- output_directory: Where to save all artifacts +- --author: Author name for artifacts +- --classification: Classification level (Public|Internal|Confidential|Restricted) + +## Integration + +This skill can be used in agents by including it in `skills_available`: + +```yaml +name: my.agent +skills_available: + - workflow.orchestrate +``` + +## Testing + +Run tests with: + +```bash +pytest skills/workflow/orchestrate/test_workflow_orchestrate.py -v +``` + +## Created By + +This skill was generated by **meta.skill**, the skill creator meta-agent. + +--- + +*Part of the Betty Framework* diff --git a/skills/workflow.orchestrate/__init__.py b/skills/workflow.orchestrate/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/workflow.orchestrate/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/workflow.orchestrate/skill.yaml b/skills/workflow.orchestrate/skill.yaml new file mode 100644 index 0000000..e1df680 --- /dev/null +++ b/skills/workflow.orchestrate/skill.yaml @@ -0,0 +1,48 @@ +name: workflow.orchestrate +version: 0.1.0 +description: Orchestrate multi-artifact workflows by coordinating specialized agents + and artifact creation skills. Creates complete artifact sets for complex initiatives + by managing dependencies, sequencing work, and ensuring artifact consistency. Supports + common SDLC workflows like project initiation, security reviews, data design, test + planning, deployment planning, and full SDLC cycles. +inputs: +- 'workflow_type (string, required): Type of workflow to execute (project-initiation, + security-review, data-design, test-planning, deployment-planning, full-sdlc)' +- 'description (string, required): Initiative or project description for context' +- 'output_directory (string, required): Directory where all workflow artifacts will + be created' +- 'author (string, optional): Author name for generated artifacts (default: "Workflow + Orchestrator")' +- 'classification (string, optional): Classification level (Public, Internal, Confidential, + Restricted, default: Internal)' +outputs: +- 'workflow-execution-report: Complete workflow execution report with all generated + artifacts' +- 'artifact-manifest: Manifest of all created artifacts with paths and validation + status' +- All artifacts specified by the workflow type (e.g., business-case, project-charter, + etc.) +- None (initiates workflows from user context) +- workflow-execution-report +- artifact-manifest +- Plus all artifacts specified by the chosen workflow type +- filesystem:read +- filesystem:write +- artifact.create skill (to create individual artifacts) +- artifact.validate skill (to validate created artifacts) +- Template files in templates/ directory +- Call artifact.create with appropriate type and context +- Call artifact.validate to check quality +- Track created artifacts and validation results +- Handle missing dependencies gracefully +- Continue workflow even if individual artifacts fail +- Report all failures in execution report +- Return overall success based on whether any artifacts were created +status: active +permissions: [] +entrypoints: +- command: /workflow/orchestrate + handler: workflow_orchestrate.py + runtime: python + description: Orchestrate multi-artifact workflows by coordinating specialized agents + and artifact creation skills diff --git a/skills/workflow.orchestrate/test_workflow_orchestrate.py b/skills/workflow.orchestrate/test_workflow_orchestrate.py new file mode 100644 index 0000000..ff9e610 --- /dev/null +++ b/skills/workflow.orchestrate/test_workflow_orchestrate.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Tests for workflow.orchestrate + +Generated by meta.skill +""" + +import pytest +import sys +import os +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from skills.workflow_orchestrate import workflow_orchestrate + + +class TestWorkflowOrchestrate: + """Tests for WorkflowOrchestrate""" + + def setup_method(self): + """Setup test fixtures""" + self.skill = workflow_orchestrate.WorkflowOrchestrate() + + def test_initialization(self): + """Test skill initializes correctly""" + assert self.skill is not None + assert self.skill.base_dir is not None + + def test_execute_basic(self): + """Test basic execution""" + result = self.skill.execute() + + assert result is not None + assert "ok" in result + assert "status" in result + + def test_execute_success(self): + """Test successful execution""" + result = self.skill.execute() + + assert result["ok"] is True + assert result["status"] == "success" + + # TODO: Add more specific tests based on skill functionality + + +def test_cli_help(capsys): + """Test CLI help message""" + sys.argv = ["workflow_orchestrate.py", "--help"] + + with pytest.raises(SystemExit) as exc_info: + workflow_orchestrate.main() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "Orchestrate multi-artifact workflows by coordinati" in captured.out + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/skills/workflow.orchestrate/workflow_orchestrate.py b/skills/workflow.orchestrate/workflow_orchestrate.py new file mode 100755 index 0000000..3a57145 --- /dev/null +++ b/skills/workflow.orchestrate/workflow_orchestrate.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +workflow.orchestrate skill - Multi-artifact workflow orchestration + +Coordinates specialized agents to create complete artifact sets for complex initiatives. +Manages dependencies, sequencing, and validation. +""" + +import sys +import os +import argparse +import yaml +from pathlib import Path +from datetime import datetime +from typing import Dict, Any, List, Optional + +from betty.skill_executor import execute_skill_in_process + + +# Workflow definitions +WORKFLOWS = { + "project-initiation": { + "description": "Complete project initiation artifact set", + "artifacts": [ + {"type": "business-case", "agent": "strategy.architect", "priority": 1}, + {"type": "project-charter", "agent": "governance.manager", "priority": 2, "depends_on": ["business-case"]}, + {"type": "raid-log", "agent": "governance.manager", "priority": 3}, + {"type": "stakeholder-analysis", "agent": "governance.manager", "priority": 3}, + ] + }, + "security-review": { + "description": "Comprehensive security assessment", + "artifacts": [ + {"type": "threat-model", "agent": "security.architect", "priority": 1}, + {"type": "security-architecture-diagram", "agent": "security.architect", "priority": 2, "depends_on": ["threat-model"]}, + {"type": "security-assessment", "agent": "security.architect", "priority": 2}, + {"type": "vulnerability-management-plan", "agent": "security.architect", "priority": 3}, + ] + }, +} + + +def create_artifact(artifact_type: str, context: str, output_path: str, metadata: Optional[Dict] = None) -> Dict[str, Any]: + """Create a single artifact using artifact.create skill""" + try: + args = [artifact_type, context, output_path] + + if metadata and metadata.get('author'): + args.extend(["--author", metadata['author']]) + + result = execute_skill_in_process("artifact.create", args, timeout=30) + + if result["returncode"] == 0: + return { + "success": True, + "artifact_path": output_path, + "artifact_type": artifact_type + } + else: + return { + "success": False, + "error": result["stderr"] or result["stdout"], + "artifact_type": artifact_type + } + + except Exception as e: + return { + "success": False, + "error": str(e), + "artifact_type": artifact_type + } + + +def orchestrate_workflow( + workflow_type: str, + context: str, + output_directory: str, + author: str = "Workflow Orchestrator" +) -> Dict[str, Any]: + """ + Orchestrate multi-artifact workflow + """ + + if workflow_type not in WORKFLOWS: + return { + "success": False, + "error": f"Unknown workflow type: {workflow_type}", + "available_workflows": list(WORKFLOWS.keys()) + } + + workflow = WORKFLOWS[workflow_type] + + # Create output directory + output_path = Path(output_directory) + output_path.mkdir(parents=True, exist_ok=True) + + # Initialize workflow report + workflow_report = { + "workflow_type": workflow_type, + "description": workflow.get("description"), + "started_at": datetime.now().isoformat(), + "artifacts_created": [], + "artifacts_failed": [], + } + + metadata = {"author": author} + + # Sort artifacts by priority + artifacts_to_create = sorted(workflow["artifacts"], key=lambda x: x.get("priority", 99)) + + # Track created artifacts + created_artifacts = {} + + print(f"\n{'='*70}") + print(f"🚀 Starting {workflow_type} workflow") + print(f"{'='*70}\n") + + # Create each artifact + for i, artifact_spec in enumerate(artifacts_to_create, 1): + artifact_type = artifact_spec["type"] + depends_on = artifact_spec.get("depends_on", []) + + print(f"[{i}/{len(artifacts_to_create)}] Creating {artifact_type}...") + + # Check dependencies + missing_deps = [dep for dep in depends_on if dep not in created_artifacts] + if missing_deps: + print(f" ⚠️ Skipping - missing dependencies: {missing_deps}\n") + workflow_report["artifacts_failed"].append({ + "artifact_type": artifact_type, + "reason": f"Missing dependencies: {missing_deps}" + }) + continue + + # Build context with dependencies + artifact_context = context + if depends_on: + dep_info = ", ".join([f"{dep} at {created_artifacts[dep]}" for dep in depends_on]) + artifact_context += f"\n\nRelated artifacts: {dep_info}" + + output_file = output_path / f"{artifact_type}.yaml" + + # Create artifact + result = create_artifact( + artifact_type=artifact_type, + context=artifact_context, + output_path=str(output_file), + metadata=metadata + ) + + if result["success"]: + print(f" ✅ Created: {output_file}\n") + created_artifacts[artifact_type] = str(output_file) + + workflow_report["artifacts_created"].append({ + "artifact_type": artifact_type, + "artifact_path": str(output_file), + "created_at": datetime.now().isoformat() + }) + else: + print(f" ❌ Failed: {result.get('error', 'Unknown error')}\n") + workflow_report["artifacts_failed"].append({ + "artifact_type": artifact_type, + "reason": result.get("error", "Unknown error") + }) + + # Calculate summary + total_artifacts = len(artifacts_to_create) + created_count = len(workflow_report["artifacts_created"]) + + workflow_report["completed_at"] = datetime.now().isoformat() + workflow_report["summary"] = { + "total_artifacts": total_artifacts, + "created": created_count, + "failed": len(workflow_report["artifacts_failed"]), + "success_rate": f"{(created_count / total_artifacts * 100):.0f}%" if total_artifacts > 0 else "0%" + } + + # Save workflow report + report_file = output_path / f"{workflow_type}-workflow-report.yaml" + with open(report_file, 'w') as f: + yaml.dump(workflow_report, f, default_flow_style=False, sort_keys=False) + + print(f"{'='*70}") + print(f"Created: {created_count}/{total_artifacts} artifacts") + print(f"📄 Report: {report_file}") + print(f"{'='*70}\n") + + return { + "success": created_count > 0, + "workflow_report": workflow_report, + "report_file": str(report_file) + } + + +def main(): + """CLI entry point""" + parser = argparse.ArgumentParser(description='Orchestrate multi-artifact workflows') + parser.add_argument('workflow_type', choices=list(WORKFLOWS.keys()), help='Workflow type') + parser.add_argument('description', help='Initiative description') + parser.add_argument('output_directory', help='Output directory') + parser.add_argument('--author', default='Workflow Orchestrator', help='Author name') + + args = parser.parse_args() + + result = orchestrate_workflow( + workflow_type=args.workflow_type, + context=args.description, + output_directory=args.output_directory, + author=args.author + ) + + return 0 if result["success"] else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/skills/workflow.validate/SKILL.md b/skills/workflow.validate/SKILL.md new file mode 100644 index 0000000..b5789bf --- /dev/null +++ b/skills/workflow.validate/SKILL.md @@ -0,0 +1,50 @@ +--- +name: Workflow Validate +description: Validates workflow YAML files to ensure structure and schema correctness. +--- + +# Workflow Validate + +## Purpose +Ensures that workflow YAML files are valid before execution. +Checks required fields (`steps`, `skill`, `args`) and field types. + +## How to Use +```bash +python skills/workflow.validate/workflow_validate.py workflows/example.yaml +``` + +## Inputs + +* `workflow_path` – Path to the workflow file. + +## Outputs + +* JSON printed to stdout with `ok`, `errors`, `status`, and `path` fields. + +## Example + +Input (`invalid_workflow.yaml`): + +```yaml +steps: + - args: ["foo"] +``` + +Output: + +```json +{ + "valid": false, + "errors": ["Step 1 missing 'skill'"], + "status": "failed" +} +``` + +## Dependencies + +* `context.schema` + +## Version + +v0.1.0 diff --git a/skills/workflow.validate/__init__.py b/skills/workflow.validate/__init__.py new file mode 100644 index 0000000..7f2729c --- /dev/null +++ b/skills/workflow.validate/__init__.py @@ -0,0 +1 @@ +# Auto-generated package initializer for skills. diff --git a/skills/workflow.validate/skill.yaml b/skills/workflow.validate/skill.yaml new file mode 100644 index 0000000..7ee4c66 --- /dev/null +++ b/skills/workflow.validate/skill.yaml @@ -0,0 +1,26 @@ +name: workflow.validate +version: 0.1.0 +description: > + Validates Betty workflow YAML definitions to ensure correct structure and required fields. +inputs: + - workflow_path +outputs: + - validation_result.json +dependencies: + - context.schema +status: active + +entrypoints: + - command: /workflow/validate + handler: workflow_validate.py + runtime: python + description: > + Validate the structure of a workflow YAML file before execution. + parameters: + - name: workflow_path + type: string + required: true + description: Path to the workflow YAML file. + permissions: + - filesystem + - read diff --git a/skills/workflow.validate/workflow_validate.py b/skills/workflow.validate/workflow_validate.py new file mode 100644 index 0000000..eb43c5f --- /dev/null +++ b/skills/workflow.validate/workflow_validate.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +"""workflow_validate.py – Implementation of the workflow.validate Skill.""" + +import json +import os +import sys +from typing import Any, Dict, List, Optional +from datetime import datetime, timezone + +import yaml +from pydantic import ValidationError as PydanticValidationError + +# Ensure project root on path for betty imports when executed directly + +from betty.errors import SkillValidationError, WorkflowError # noqa: E402 +from betty.logging_utils import setup_logger # noqa: E402 +from betty.validation import ValidationError, validate_path # noqa: E402 +from betty.telemetry_integration import telemetry_tracked # noqa: E402 +from betty.models import WorkflowDefinition # noqa: E402 +from betty.config import REGISTRY_DIR # noqa: E402 +from betty.versioning import satisfies # noqa: E402 + +logger = setup_logger(__name__) + +REQUIRED_FIELDS = ["steps"] +# Steps can have either 'skill' or 'agent' (not both) +# For skill steps: 'skill', 'version', and 'args' are required +# For agent steps: 'agent' is required, 'input' is optional + +SKILLS_REGISTRY_FILE = os.path.join(REGISTRY_DIR, "skills.json") +LOCKFILE_DIR = os.path.join(REGISTRY_DIR, "runs") + + +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_workflow(path: str) -> Dict[str, Any]: + """Load a workflow YAML file into a dictionary.""" + try: + with open(path, "r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) + except FileNotFoundError as exc: + raise WorkflowError(f"Workflow file not found: {path}") from exc + except yaml.YAMLError as exc: + raise SkillValidationError(f"Invalid YAML syntax: {exc}") from exc + + if data is None: + return {} + + if not isinstance(data, dict): + raise SkillValidationError("Workflow root must be a mapping") + + return data + + +def _validate_required_fields(data: Dict[str, Any]) -> List[str]: + """Validate presence of required top-level workflow fields.""" + errors: List[str] = [] + for field in REQUIRED_FIELDS: + if field not in data: + errors.append(f"Missing required field: {field}") + return errors + + +def _load_skills_registry() -> Dict[str, Any]: + """Load the skills registry from disk.""" + try: + if not os.path.exists(SKILLS_REGISTRY_FILE): + logger.warning(f"Skills registry not found at {SKILLS_REGISTRY_FILE}") + return {"skills": []} + + with open(SKILLS_REGISTRY_FILE, 'r') as f: + return json.load(f) + except Exception as e: + logger.error(f"Failed to load skills registry: {e}") + return {"skills": []} + + +def _resolve_skill_version(skill_name: str, version_constraint: str, registry: Dict[str, Any]) -> Optional[str]: + """ + Resolve a skill version from the registry that satisfies the constraint. + + Args: + skill_name: Name of the skill to resolve + version_constraint: Version constraint (e.g., ">=1.0.0 <2.0.0") + registry: Skills registry data + + Returns: + Resolved version string, or None if no matching version found + """ + matching_versions = [] + + for skill in registry.get("skills", []): + if skill.get("name") == skill_name: + skill_version = skill.get("version") + if skill_version and satisfies(skill_version, version_constraint): + matching_versions.append(skill_version) + + if not matching_versions: + return None + + # Return the latest version that satisfies the constraint + # (assuming versions are stored in order, or we could sort them) + return matching_versions[-1] + + +def _validate_steps(steps: Any) -> List[str]: + """Validate the steps section of the workflow.""" + errors: List[str] = [] + + if not isinstance(steps, list): + errors.append("`steps` must be a list") + return errors + + for index, step in enumerate(steps, start=1): + if not isinstance(step, dict): + errors.append(f"Step {index} must be a mapping") + continue + + # Check if step has skill or agent field + has_skill = "skill" in step + has_agent = "agent" in step + + if not has_skill and not has_agent: + errors.append(f"Step {index} must have either 'skill' or 'agent' field") + continue + + if has_skill and has_agent: + errors.append(f"Step {index} cannot have both 'skill' and 'agent' fields") + continue + + # Validate skill steps + if has_skill: + skill_value = step.get("skill") + if not isinstance(skill_value, str): + errors.append(f"Step {index} 'skill' must be a string") + + # version field is required for skill steps + if "version" not in step: + errors.append(f"Step {index} missing 'version' constraint (required for skill steps)") + else: + version_value = step.get("version") + if not isinstance(version_value, str): + errors.append(f"Step {index} 'version' must be a string") + + # args field is required for skill steps + if "args" not in step: + errors.append(f"Step {index} missing 'args' field (required for skill steps)") + else: + args_value = step.get("args") + if not isinstance(args_value, list): + errors.append(f"Step {index} 'args' must be a list") + + # Validate agent steps + if has_agent: + agent_value = step.get("agent") + if not isinstance(agent_value, str): + errors.append(f"Step {index} 'agent' must be a string") + + # input field is optional for agent steps, but if present must be a string + input_value = step.get("input") + if input_value is not None and not isinstance(input_value, str): + errors.append(f"Step {index} 'input' must be a string") + + return errors + + +def _validate_with_pydantic(data: Dict[str, Any]) -> List[str]: + """ + Validate workflow data using Pydantic schema. + + Args: + data: Workflow data dictionary + + Returns: + List of validation errors (empty if valid) + """ + errors: List[str] = [] + + try: + # Attempt Pydantic validation + WorkflowDefinition.model_validate(data) + logger.info("Pydantic schema validation passed") + except PydanticValidationError as exc: + logger.warning("Pydantic schema validation failed") + # Convert Pydantic errors to human-readable messages + 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 _resolve_versions_and_create_lockfile( + workflow_name: str, + workflow_data: Dict[str, Any], + registry: Dict[str, Any] +) -> Dict[str, Any]: + """ + Resolve skill versions from registry and create a lockfile. + + Args: + workflow_name: Name of the workflow + workflow_data: Workflow definition data + registry: Skills registry data + + Returns: + Dictionary with resolved versions and lockfile path + + Raises: + WorkflowError: If version resolution fails + """ + resolved = [] + errors = [] + + for index, step in enumerate(workflow_data.get("steps", []), start=1): + if "skill" in step: + skill_name = step.get("skill") + version_constraint = step.get("version") + + if skill_name and version_constraint: + resolved_version = _resolve_skill_version(skill_name, version_constraint, registry) + + if resolved_version: + resolved.append({ + "skill": skill_name, + "version": resolved_version, + "constraint": version_constraint + }) + else: + errors.append( + f"Step {index}: No version of skill '{skill_name}' " + f"satisfies constraint '{version_constraint}'" + ) + + if errors: + raise WorkflowError( + f"Version resolution failed for workflow '{workflow_name}':\n" + + "\n".join(f" - {err}" for err in errors) + ) + + # Create lockfile + timestamp = datetime.now(timezone.utc).isoformat() + lockfile_data = { + "workflow": workflow_name, + "timestamp": timestamp, + "resolved": resolved + } + + # Ensure lockfile directory exists + os.makedirs(LOCKFILE_DIR, exist_ok=True) + + # Generate lockfile name + lockfile_name = f"{timestamp.replace(':', '-').replace('.', '-')}.lock.json" + lockfile_path = os.path.join(LOCKFILE_DIR, lockfile_name) + + # Write lockfile + try: + with open(lockfile_path, 'w') as f: + json.dump(lockfile_data, f, indent=2) + logger.info(f"Lockfile created at {lockfile_path}") + except Exception as e: + logger.error(f"Failed to create lockfile: {e}") + raise WorkflowError(f"Failed to create lockfile: {e}") + + return { + "resolved": resolved, + "lockfile_path": lockfile_path, + "lockfile_data": lockfile_data + } + + +def validate_workflow(path: str) -> Dict[str, Any]: + """ + Validate a workflow definition file. + + Validates workflow structure, version constraints, and resolves skill versions + from the registry. On success, creates a lockfile under registry/runs/. + + Args: + path: Path to workflow YAML file + + Returns: + Validation result dictionary + + Raises: + SkillValidationError: If validation fails + WorkflowError: If version resolution fails + """ + try: + validate_path(path, must_exist=True) + except ValidationError as exc: + raise SkillValidationError(str(exc)) from exc + + workflow_data = _load_workflow(path) + + errors: List[str] = [] + + # First, validate with Pydantic schema + schema_errors = _validate_with_pydantic(workflow_data) + errors.extend(schema_errors) + + # Then run existing validation for more specific checks + errors.extend(_validate_required_fields(workflow_data)) + errors.extend(_validate_steps(workflow_data.get("steps", []))) + + if errors: + status = "failed" + result = { + "valid": False, + "errors": errors, + "status": status, + "path": path, + } + return result + + # If validation passed, resolve versions and create lockfile + workflow_name = workflow_data.get("name", os.path.basename(path).replace(".yaml", "")) + registry = _load_skills_registry() + + try: + lockfile_info = _resolve_versions_and_create_lockfile(workflow_name, workflow_data, registry) + + result = { + "valid": True, + "errors": [], + "status": "validated", + "path": path, + "lockfile": lockfile_info["lockfile_path"], + "resolved_versions": lockfile_info["resolved"], + } + except WorkflowError as e: + # Version resolution failed + result = { + "valid": False, + "errors": [str(e)], + "status": "failed", + "path": path, + } + + return result + + +@telemetry_tracked(skill_name="workflow.validate", caller="cli") +def main(argv: Optional[List[str]] = None) -> int: + """Entry point for CLI execution.""" + argv = argv or sys.argv[1:] + + if len(argv) != 1: + message = "Usage: workflow_validate.py " + logger.error(message) + response = build_response( + False, + path="", + errors=[message], + details={"error": {"error": "UsageError", "message": message, "details": {}}}, + ) + print(json.dumps(response, indent=2)) + return 1 + + workflow_path = argv[0] + + try: + result = validate_workflow(workflow_path) + + # Check if there are schema validation errors + has_schema_errors = any("Schema validation error" in err for err in result.get("errors", [])) + + details = result.copy() + if not result.get("valid", False) and has_schema_errors: + details["error"] = { + "type": "SchemaError", + "error": "SchemaError", + "message": "Workflow schema validation failed", + "details": {"errors": result.get("errors", [])} + } + + response = build_response( + result.get("valid", False), + path=result.get("path", workflow_path), + errors=result.get("errors", []), + details=details, + ) + print(json.dumps(response, indent=2)) + return 0 if response["ok"] else 1 + except (SkillValidationError, WorkflowError) as exc: + logger.error("Validation failed: %s", exc) + response = build_response( + False, + path=workflow_path, + errors=[str(exc)], + details={"error": {"error": type(exc).__name__, "message": str(exc), "details": {}}}, + ) + print(json.dumps(response, indent=2)) + return 1 + except Exception as exc: # pragma: no cover - unexpected failures + logger.exception("Unexpected error during workflow validation") + response = build_response( + False, + path=workflow_path, + errors=[str(exc)], + details={"error": {"error": type(exc).__name__, "message": str(exc)}}, + ) + print(json.dumps(response, indent=2)) + return 1 + + +if __name__ == "__main__": # pragma: no cover - CLI entry point + sys.exit(main(sys.argv[1:]))