✨ feat(skills): implement full skills system with three-tier architecture

- add foundational directory structure under src/skills with apps, modules, handlers, catalog, templates, tests, and trinity files
- support three skill tiers: markdown-only, markdown+handler.py, and full 3-layer apps/modules/handlers
- implement skill discovery scanning search paths (.aipass/skills, ~/.aipass/skills, src/skills/catalog)
- support SKILL.md with YAML frontmatter, compatible with OpenClaw format
- implement loader to parse full SKILL.md and optionally import handler.py module
- implement runner to execute skill handlers or display markdown instructions if no handler
- add skill creator scaffolding from tiered templates with placeholder substitution
- add skill registry for caching discovered skills and lookup by name
- add validator handler to check pip packages, CLI binaries, and environment config requirements
- add built-in test skills: github (markdown), system_status (handler), drone_commands (full 3-layer)
- implement drone_commands skill with subprocess executor, output parser, and command runner modules
- provide detailed README documenting architecture, usage, SKILL.md format, and commands
- add comprehensive unit and integration tests covering discovery, loading, running, validation, and lifecycle
- create seedgo standards pack with skill format, handler contract, and structure checks and documentation
- use consistent coding standards: handlers return dicts and never print; modules can print for orchestration
- enable drone routing with `drone @skills <command>` supporting list, info, run, create, validate commands
This commit is contained in:
AIOSAI
2026-03-07 12:15:29 -08:00
parent 22c33d4ec7
commit 330708695d
56 changed files with 4950 additions and 0 deletions
@@ -0,0 +1,116 @@
# FPLAN-0006 - [framework] Skills System Build
**Created**: 2026-03-07
**Branch**: /home/aipass/aipass_business/AIPass
**Status**: Complete
**Type**: Master Plan
---
## Planning Phase
### Goal
Build the Skills system for AIPass framework. Skills are documented capabilities that any AI can use — from simple markdown SOPs to full 3-layer code implementations. Lives at `src/skills/` (peer to `src/aipass/`, not inside it — skills are separate from infrastructure).
### Approach
Three-tier skill architecture:
- **Tier 1 (Markdown):** SKILL.md only — instructions an LLM reads and follows
- **Tier 2 (Markdown + Handler):** SKILL.md + handler.py — code does the work
- **Tier 3 (Full 3-layer):** SKILL.md + apps/modules/handlers — complete implementation
Follows seedgo's pack pattern: auto-discovery via SKILL.md manifests, drone-routable via `handle_command()`.
OpenClaw SKILL.md format compatibility: their 52 skills (MIT license) can be adapted with minor metadata changes.
### Key Decisions
- **Location**: `src/skills/` — outside `src/aipass/` (skills are capabilities, not infrastructure)
- **Format**: SKILL.md with YAML frontmatter (compatible with OpenClaw format)
- **Discovery**: Glob + importlib + duck typing (same pattern as Nexus skills, seedgo packs)
- **Drone integration**: `drone @skills list|info|run|create|validate`
- **Handler contract**: `run(action, args, config) -> {"success": bool, "output": str, "error": str|None}`
- **No branch manager build** — directory structure only (like FPLAN-0003/0004/0005 pattern)
### Reference Documents
- Design: `vera/projects/framework/skills_system_design.md`
- Research: `vera/projects/framework/nexus_and_skills_design.md`
- Seedgo pattern: `src/aipass/seedgo/apps/standards/aipass/` (pack architecture)
- Telegram reference: `/home/aipass/aipass_core/api/apps/handlers/telegram/` (complex skill example)
- OpenClaw skills: `/home/aipass/external_repos/openclaw/skills/` (SKILL.md format reference)
- Architecture: `vera/projects/framework/architecture_reference.md`
---
## Execution Log
### Phase 1: Foundation — Directory Structure + Core
- [x] Create `src/skills/` directory structure (3-layer: apps/skills.py → modules/ → handlers/)
- [x] Create entry point: `apps/skills.py` with `handle_command()` for drone routing
- [x] Create discovery module: `apps/modules/discovery.py` (scan search paths, parse SKILL.md frontmatter)
- [x] Create loader module: `apps/modules/loader.py` (load full SKILL.md, import handler if present)
- [x] Create runner module: `apps/modules/runner.py` (execute handler or display instructions)
- [x] Create creator module: `apps/modules/creator.py` (scaffold new skills from templates)
- [x] Create registry handler: `apps/handlers/registry.py` (skill registry management)
- [x] Create validator handler: `apps/handlers/validator.py` (check requirements — bins, pip, config)
- [x] Create template handler: `apps/handlers/template.py` (skill templates for scaffolding)
- [x] Create Trinity files: `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`
- [x] Create `__init__.py` files at all levels
- [x] Create README.md
### Phase 2: Templates
- [x] Markdown-only skill template (`templates/markdown_only/SKILL.md`)
- [x] Markdown + handler skill template (`templates/with_handler/SKILL.md` + `handler.py`)
- [x] Full 3-layer skill template (`templates/full/SKILL.md` + `apps/` structure)
### Phase 3: Test Skills (One Per Tier)
- [x] Tier 1: Adapt OpenClaw GitHub skill → `catalog/github/SKILL.md`
- [x] Tier 2: Build system_status skill → `catalog/system_status/SKILL.md` + `handler.py`
- [x] Tier 3: Create drone_commands skill skeleton → `catalog/drone_commands/` with apps structure
### Phase 4: Seedgo Skills Standard
- [x] Create skills standard pack: `src/aipass/seedgo/apps/standards/skills/`
- [x] Create `pack.json` manifest
- [x] Create `pack_entry.py` entry point
- [x] Create standard modules (skill_format, skill_structure, skill_handler)
- [x] Create standard handlers (content + check pairs)
### Phase 5: Testing
- [x] Unit tests for discovery engine (32 tests)
- [x] Unit tests for loader (7 tests)
- [x] Unit tests for runner (11 tests)
- [x] Unit tests for validator (10 tests)
- [x] Integration test: full skill lifecycle create → discover → load → run (14 tests)
- [x] Test OpenClaw skill adaptation works (github skill loads and runs correctly)
---
## Notes
- Same citizen-branch pattern as drone, ai_mail, prax — but directory structure only, no branch manager
- Skills are separate from aipass infrastructure (sit at `src/skills/`, not `src/aipass/skills/`)
- Patrick wants all three tiers supported: markdown SOPs, markdown+handler, full 3-layer
- Code skills cost zero tokens to run — markdown skills cost tokens every time. Both have a place.
- OpenClaw SKILL.md compatibility lets users adapt 52 existing MIT-licensed skills
- Seedgo standard for skills ensures all skills meet quality bar
---
## Completion Checklist
### Definition of Done
- Skills branch exists at `src/skills/` with full 3-layer architecture
- `drone @skills list|info|run|create|validate` commands work
- All three skill tiers supported (markdown, markdown+handler, full 3-layer)
- At least one test skill per tier working
- OpenClaw GitHub skill successfully adapted
- Seedgo skills standard created with at least 3 checks
- Tests passing
- Trinity files in place
---
## Close Command
When all boxes checked:
```bash
drone @flow close FPLAN-0006
```
@@ -0,0 +1 @@
# Skills Standards Pack - Standards for AIPass Skills system
@@ -0,0 +1 @@
# Extensions package - Drop-in extensions for skills functionality
@@ -0,0 +1,6 @@
"""
Skills Standards Pack - Handlers
Implementation layer for Skills standards checking.
Organized by domain: standards/
"""
@@ -0,0 +1,8 @@
"""
Skills Standards Checkers
Handlers for validating skill compliance with AIPass Skills standards.
Each handler checks one specific standard domain.
"""
__version__ = "0.1.0"
@@ -0,0 +1,302 @@
"""
Skill Format Standards Checker Handler
Validates SKILL.md format compliance for AIPass skills.
Checks frontmatter parsing, required fields, version format, handler consistency.
"""
# =================== META ====================
# Name: skill_format_check.py
# Description: Skill Format Standards Checker Handler
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import re
from pathlib import Path
from typing import Dict, List
def _parse_frontmatter(content: str) -> Dict:
"""Parse YAML frontmatter from SKILL.md content.
Handles simple key: value pairs without requiring PyYAML.
Supports strings, booleans, and lists.
Args:
content: Full file content of SKILL.md
Returns:
dict with parsed frontmatter, or empty dict if invalid
"""
lines = content.split("\n")
# Find frontmatter delimiters
if not lines or lines[0].strip() != "---":
return {}
end_index = None
for i in range(1, len(lines)):
if lines[i].strip() == "---":
end_index = i
break
if end_index is None:
return {}
# Parse simple YAML key: value pairs
frontmatter = {}
current_key = None
for line in lines[1:end_index]:
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
# List item (continuation of previous key)
if stripped.startswith("- ") and current_key is not None:
if not isinstance(frontmatter[current_key], list):
frontmatter[current_key] = []
frontmatter[current_key].append(stripped[2:].strip())
continue
# Key: value pair
if ":" in stripped:
key, _, value = stripped.partition(":")
key = key.strip()
value = value.strip()
current_key = key
# Parse booleans
if value.lower() in ("true", "yes"):
frontmatter[key] = True
elif value.lower() in ("false", "no"):
frontmatter[key] = False
elif value == "":
# Could be a list starting on next line
frontmatter[key] = ""
else:
frontmatter[key] = value
return frontmatter
def _is_valid_semver(version: str) -> bool:
"""Check if version string follows semver format.
Args:
version: Version string to validate
Returns:
bool: True if valid semver
"""
pattern = r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$"
return bool(re.match(pattern, version))
def check_skill_format(skill_path: str) -> Dict:
"""
Check if skill has valid SKILL.md format
Args:
skill_path: Path to skill directory
Returns:
dict: {
'passed': bool,
'checks': list,
'score': float,
'standard': str
}
"""
checks: List[Dict] = []
path = Path(skill_path)
# Validate skill directory exists
if not path.exists() or not path.is_dir():
return {
"passed": False,
"checks": [
{
"name": "Skill directory",
"passed": False,
"message": f"Skill directory not found: {skill_path}",
}
],
"score": 0.0,
"standard": "SKILL_FORMAT",
}
# Check 1: SKILL.md exists
skill_md = path / "SKILL.md"
if not skill_md.exists():
checks.append(
{
"name": "SKILL.md exists",
"passed": False,
"message": "SKILL.md not found in skill directory",
}
)
# Cannot continue without SKILL.md
return {
"passed": False,
"checks": checks,
"score": 0.0,
"standard": "SKILL_FORMAT",
}
checks.append(
{
"name": "SKILL.md exists",
"passed": True,
"message": "SKILL.md found",
}
)
# Read SKILL.md
try:
content = skill_md.read_text(encoding="utf-8")
except Exception as e:
checks.append(
{
"name": "SKILL.md readable",
"passed": False,
"message": f"Error reading SKILL.md: {e}",
}
)
return {
"passed": False,
"checks": checks,
"score": 0.0,
"standard": "SKILL_FORMAT",
}
# Check 2: YAML frontmatter is parseable
frontmatter = _parse_frontmatter(content)
if not frontmatter:
checks.append(
{
"name": "YAML frontmatter",
"passed": False,
"message": "No valid YAML frontmatter found (must start with --- and end with ---)",
}
)
# Cannot check fields without frontmatter
passed_count = sum(1 for c in checks if c["passed"])
total = len(checks)
score = (passed_count / total * 100) if total > 0 else 0.0
return {
"passed": score >= 75,
"checks": checks,
"score": score,
"standard": "SKILL_FORMAT",
}
checks.append(
{
"name": "YAML frontmatter",
"passed": True,
"message": "Frontmatter parsed successfully",
}
)
# Check 3: name field present and non-empty
name_val = frontmatter.get("name", "")
if name_val and isinstance(name_val, str) and name_val.strip():
checks.append(
{
"name": "name field",
"passed": True,
"message": f"name: {name_val}",
}
)
else:
checks.append(
{
"name": "name field",
"passed": False,
"message": "name field missing or empty in frontmatter",
}
)
# Check 4: description field present and non-empty
desc_val = frontmatter.get("description", "")
if desc_val and isinstance(desc_val, str) and desc_val.strip():
checks.append(
{
"name": "description field",
"passed": True,
"message": f"description: {desc_val[:60]}",
}
)
else:
checks.append(
{
"name": "description field",
"passed": False,
"message": "description field missing or empty in frontmatter",
}
)
# Check 5: has_handler consistency
has_handler = frontmatter.get("has_handler")
if has_handler is True:
handler_py = path / "handler.py"
if handler_py.exists():
checks.append(
{
"name": "has_handler consistency",
"passed": True,
"message": "has_handler: true and handler.py exists",
}
)
else:
checks.append(
{
"name": "has_handler consistency",
"passed": False,
"message": "has_handler: true but handler.py not found",
}
)
elif has_handler is not None:
checks.append(
{
"name": "has_handler consistency",
"passed": True,
"message": "has_handler: false (no handler expected)",
}
)
# Check 6: version format (semver) if present
version_val = frontmatter.get("version", "")
if version_val and isinstance(version_val, str):
if _is_valid_semver(version_val):
checks.append(
{
"name": "version format",
"passed": True,
"message": f"version: {version_val} (valid semver)",
}
)
else:
checks.append(
{
"name": "version format",
"passed": False,
"message": f"version: {version_val} (invalid semver - expected X.Y.Z)",
}
)
# Calculate score
passed_count = sum(1 for c in checks if c["passed"])
total = len(checks)
score = (passed_count / total * 100) if total > 0 else 0.0
return {
"passed": score >= 75,
"checks": checks,
"score": score,
"standard": "SKILL_FORMAT",
}
@@ -0,0 +1,70 @@
"""
Skill Format Standards Content Handler
Provides formatted SKILL.md format standards content.
Module orchestrates, handler implements.
"""
# =================== META ====================
# Name: skill_format_content.py
# Description: Skill Format Standards Content Handler
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
def get_skill_format_standards() -> str:
"""Return formatted skill format standards content with Rich markup
Returns:
str: Formatted standards text with Rich styling
"""
lines = [
"[bold cyan]CORE PRINCIPLE:[/bold cyan]",
" Every skill MUST have a SKILL.md with valid YAML frontmatter",
" The frontmatter is the skill's identity - without it, the skill is invisible",
"",
"[bold cyan]REQUIRED FIELDS:[/bold cyan]",
"",
" [yellow]name:[/yellow] Skill name (non-empty string)",
" [yellow]description:[/yellow] What the skill does (non-empty string)",
"",
"[bold cyan]OPTIONAL FIELDS:[/bold cyan]",
"",
" [dim]version:[/dim] Semver format (e.g., 1.0.0, 0.2.1)",
" [dim]has_handler:[/dim] Boolean - if true, handler.py must exist",
" [dim]author:[/dim] Who created the skill",
" [dim]tags:[/dim] List of categorization tags",
"",
"[bold cyan]VALID SKILL.md FORMAT:[/bold cyan]",
"",
" [dim]---[/dim]",
" [dim]name: my_skill[/dim]",
" [dim]description: Does something useful[/dim]",
" [dim]version: 1.0.0[/dim]",
" [dim]has_handler: true[/dim]",
" [dim]---[/dim]",
"",
" [dim]# My Skill[/dim]",
" [dim]Detailed documentation below the frontmatter...[/dim]",
"",
"[yellow]CHECKS PERFORMED:[/yellow]",
"",
" 1. [bold]SKILL.md exists[/bold] in skill directory",
" 2. [bold]YAML frontmatter parseable[/bold] (valid --- delimited block)",
" 3. [bold]name field[/bold] present and non-empty",
" 4. [bold]description field[/bold] present and non-empty",
" 5. [bold]has_handler consistency[/bold] - if true, handler.py must exist",
" 6. [bold]version format[/bold] - semver if present",
"",
"[yellow]WARNINGS:[/yellow]",
" - Missing SKILL.md = [red]skill is invisible to the system[/red]",
" - Invalid YAML = [red]skill metadata cannot be loaded[/red]",
" - has_handler: true without handler.py = [red]broken skill[/red]",
"",
"[bold cyan]REFERENCE:[/bold cyan]",
" [dim]See: seedgo standards pack (skills)[/dim]",
]
return "\n".join(lines)
@@ -0,0 +1,297 @@
"""
Skill Handler Standards Checker Handler
Validates skill handler contract compliance for AIPass skills.
Checks run() function signature, return annotations, print usage, get_actions().
"""
# =================== META ====================
# Name: skill_handler_check.py
# Description: Skill Handler Standards Checker Handler
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import ast
from pathlib import Path
from typing import Dict, List, Optional
def _find_function(tree: ast.Module, name: str) -> Optional[ast.FunctionDef]:
"""Find a top-level function by name in an AST tree.
Args:
tree: Parsed AST module
name: Function name to find
Returns:
FunctionDef node or None
"""
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.FunctionDef) and node.name == name:
return node
return None
def _get_param_names(func_node: ast.FunctionDef) -> List[str]:
"""Extract parameter names from a function definition.
Args:
func_node: AST FunctionDef node
Returns:
List of parameter name strings (excludes 'self')
"""
params = []
for arg in func_node.args.args:
if arg.arg != "self":
params.append(arg.arg)
return params
def _has_print_calls(tree: ast.Module) -> List[int]:
"""Find all print() calls in the AST and return their line numbers.
Args:
tree: Parsed AST module
Returns:
List of line numbers where print() is called
"""
print_lines = []
for node in ast.walk(tree):
if isinstance(node, ast.Call):
# print(...)
if isinstance(node.func, ast.Name) and node.func.id == "print":
print_lines.append(node.lineno)
# builtins.print(...)
elif (
isinstance(node.func, ast.Attribute)
and node.func.attr == "print"
and isinstance(node.func.value, ast.Name)
and node.func.value.id == "builtins"
):
print_lines.append(node.lineno)
return print_lines
def _get_return_annotation(func_node: ast.FunctionDef) -> Optional[str]:
"""Get the return type annotation of a function as a string.
Args:
func_node: AST FunctionDef node
Returns:
String representation of return annotation, or None
"""
if func_node.returns is None:
return None
if isinstance(func_node.returns, ast.Name):
return func_node.returns.id
elif isinstance(func_node.returns, ast.Constant):
return str(func_node.returns.value)
elif isinstance(func_node.returns, ast.Attribute):
return func_node.returns.attr
return "complex"
def check_skill_handler(handler_path: str) -> Dict:
"""
Check if skill handler implements the standard contract
Args:
handler_path: Path to handler.py file
Returns:
dict: {
'passed': bool,
'checks': list,
'score': float,
'standard': str
}
"""
checks: List[Dict] = []
path = Path(handler_path)
# Validate file exists
if not path.exists():
return {
"passed": False,
"checks": [
{
"name": "handler.py exists",
"passed": False,
"message": f"File not found: {handler_path}",
}
],
"score": 0.0,
"standard": "SKILL_HANDLER",
}
# Parse the handler file
try:
source = path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(path))
except SyntaxError as e:
return {
"passed": False,
"checks": [
{
"name": "Syntax valid",
"passed": False,
"message": f"Syntax error in handler: {e}",
}
],
"score": 0.0,
"standard": "SKILL_HANDLER",
}
except (UnicodeDecodeError, OSError) as e:
return {
"passed": False,
"checks": [
{
"name": "File readable",
"passed": False,
"message": f"Error reading handler: {e}",
}
],
"score": 0.0,
"standard": "SKILL_HANDLER",
}
# Check 1: handler.py has a run() function
run_func = _find_function(tree, "run")
if run_func is None:
checks.append(
{
"name": "run() function",
"passed": False,
"message": "run() function not found at module level",
}
)
# Cannot check parameters without run()
passed_count = sum(1 for c in checks if c["passed"])
total = len(checks)
score = (passed_count / total * 100) if total > 0 else 0.0
return {
"passed": False,
"checks": checks,
"score": score,
"standard": "SKILL_HANDLER",
}
checks.append(
{
"name": "run() function",
"passed": True,
"message": "run() function found",
}
)
# Check 2: run() accepts action, args, config parameters
param_names = _get_param_names(run_func)
required_params = ["action", "args", "config"]
missing_params = [p for p in required_params if p not in param_names]
if not missing_params:
checks.append(
{
"name": "run() parameters",
"passed": True,
"message": f"run() accepts: {', '.join(param_names)}",
}
)
else:
checks.append(
{
"name": "run() parameters",
"passed": False,
"message": f"run() missing parameters: {', '.join(missing_params)} (has: {', '.join(param_names)})",
}
)
# Check 3: run() return type annotation is dict (if present)
return_annotation = _get_return_annotation(run_func)
if return_annotation is None:
checks.append(
{
"name": "run() return annotation",
"passed": True,
"message": "No return annotation (acceptable - annotation is optional)",
}
)
elif return_annotation.lower() == "dict":
checks.append(
{
"name": "run() return annotation",
"passed": True,
"message": "run() -> dict (correct)",
}
)
else:
checks.append(
{
"name": "run() return annotation",
"passed": False,
"message": f"run() -> {return_annotation} (expected dict)",
}
)
# Check 4: No print() calls in handler code
print_lines = _has_print_calls(tree)
if print_lines:
lines_str = ", ".join(str(ln) for ln in print_lines[:5])
suffix = f" (and {len(print_lines) - 5} more)" if len(print_lines) > 5 else ""
checks.append(
{
"name": "No print() calls",
"passed": False,
"message": f"print() found on lines: {lines_str}{suffix} (handlers should return dicts, not print)",
}
)
else:
checks.append(
{
"name": "No print() calls",
"passed": True,
"message": "No print() calls found (handlers return structured data)",
}
)
# Check 5: get_actions() function exists (WARNING level - still passes)
get_actions_func = _find_function(tree, "get_actions")
if get_actions_func is not None:
checks.append(
{
"name": "get_actions() function",
"passed": True,
"message": "get_actions() found (enables introspection)",
}
)
else:
# WARNING level - counts as passed but notes the recommendation
checks.append(
{
"name": "get_actions() function",
"passed": True,
"message": "WARNING: get_actions() not found (recommended for discoverability)",
}
)
# Calculate score
passed_count = sum(1 for c in checks if c["passed"])
total = len(checks)
score = (passed_count / total * 100) if total > 0 else 0.0
return {
"passed": score >= 75,
"checks": checks,
"score": score,
"standard": "SKILL_HANDLER",
}
@@ -0,0 +1,78 @@
"""
Skill Handler Standards Content Handler
Provides formatted handler contract standards content.
Module orchestrates, handler implements.
"""
# =================== META ====================
# Name: skill_handler_content.py
# Description: Skill Handler Standards Content Handler
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
def get_skill_handler_standards() -> str:
"""Return formatted skill handler standards content with Rich markup
Returns:
str: Formatted standards text with Rich styling
"""
lines = [
"[bold cyan]CORE PRINCIPLE:[/bold cyan]",
" Skill handlers implement a standard contract for predictable execution",
" Handlers never print, always return structured dicts",
"",
"[bold cyan]REQUIRED CONTRACT:[/bold cyan]",
"",
" [yellow]run(action, args, config) -> dict[/yellow]",
"",
" Parameters:",
" [dim]action[/dim] - str: The action to perform",
" [dim]args[/dim] - dict: Arguments for the action",
" [dim]config[/dim] - dict: Configuration/context",
"",
" Return keys:",
" [dim]success[/dim] - bool: Whether the action succeeded",
" [dim]output[/dim] - any: Result data",
" [dim]error[/dim] - str or None: Error message if failed",
"",
"[bold cyan]RECOMMENDED:[/bold cyan]",
"",
" [yellow]get_actions() -> list[/yellow]",
"",
" Returns list of supported action names",
" Enables introspection and auto-discovery",
"",
"[bold cyan]EXAMPLE HANDLER:[/bold cyan]",
"",
' [dim]def run(action, args, config):[/dim]',
' [dim] if action == "greet":[/dim]',
' [dim] name = args.get("name", "world")[/dim]',
' [dim] return {"success": True, "output": f"Hello {name}", "error": None}[/dim]',
' [dim] return {"success": False, "output": None, "error": f"Unknown action: {action}"}[/dim]',
"",
' [dim]def get_actions():[/dim]',
' [dim] return ["greet"][/dim]',
"",
"[yellow]CHECKS PERFORMED:[/yellow]",
"",
" 1. [bold]handler.py has run() function[/bold]",
" 2. [bold]run() accepts action, args, config[/bold] parameters",
" 3. [bold]run() return annotation[/bold] is dict (if present)",
" 4. [bold]No print() calls[/bold] in handler code",
" 5. [bold]get_actions() exists[/bold] (WARNING level, not ERROR)",
"",
"[yellow]WARNINGS:[/yellow]",
" - print() in handlers = [red]breaks structured output[/red]",
" - Missing run() = [red]handler cannot be invoked[/red]",
" - Wrong parameters = [red]caller will crash[/red]",
" - Missing get_actions() = [yellow]reduced discoverability[/yellow]",
"",
"[bold cyan]REFERENCE:[/bold cyan]",
" [dim]See: seedgo standards pack (skills)[/dim]",
]
return "\n".join(lines)
@@ -0,0 +1,236 @@
"""
Skill Structure Standards Checker Handler
Validates skill directory structure compliance for AIPass skills.
Checks tier detection, required files, handler contract, 3-layer subdirs, stray files.
"""
# =================== META ====================
# Name: skill_structure_check.py
# Description: Skill Structure Standards Checker Handler
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import ast
from pathlib import Path
from typing import Dict, List
# Valid items allowed in a skill root directory
VALID_ROOT_ITEMS = {
"SKILL.md",
"handler.py",
"__init__.py",
"apps",
"tests",
"docs",
"requirements.txt",
".gitignore",
"__pycache__",
}
def _handler_has_run(handler_path: Path) -> bool:
"""Check if handler.py has a run() function using AST parsing.
Args:
handler_path: Path to handler.py
Returns:
bool: True if run() function found at module level
"""
try:
source = handler_path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(handler_path))
except (SyntaxError, UnicodeDecodeError, OSError):
return False
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.FunctionDef) and node.name == "run":
return True
return False
def check_skill_structure(skill_path: str) -> Dict:
"""
Check if skill follows valid directory structure
Args:
skill_path: Path to skill directory
Returns:
dict: {
'passed': bool,
'checks': list,
'score': float,
'standard': str
}
"""
checks: List[Dict] = []
path = Path(skill_path)
# Check 1: Skill directory exists and is a directory
if not path.exists():
return {
"passed": False,
"checks": [
{
"name": "Skill directory exists",
"passed": False,
"message": f"Directory not found: {skill_path}",
}
],
"score": 0.0,
"standard": "SKILL_STRUCTURE",
}
if not path.is_dir():
return {
"passed": False,
"checks": [
{
"name": "Skill directory exists",
"passed": False,
"message": f"Path is not a directory: {skill_path}",
}
],
"score": 0.0,
"standard": "SKILL_STRUCTURE",
}
checks.append(
{
"name": "Skill directory exists",
"passed": True,
"message": f"Directory found: {path.name}/",
}
)
# Check 2: SKILL.md exists (required for all tiers)
skill_md = path / "SKILL.md"
if skill_md.exists():
checks.append(
{
"name": "SKILL.md exists",
"passed": True,
"message": "SKILL.md found (required for all tiers)",
}
)
else:
checks.append(
{
"name": "SKILL.md exists",
"passed": False,
"message": "SKILL.md missing (required for all tiers)",
}
)
# Detect tier
has_handler = (path / "handler.py").exists()
has_apps = (path / "apps").exists()
# Check 3: If handler.py exists, it must have a run() function
if has_handler:
handler_path = path / "handler.py"
if _handler_has_run(handler_path):
checks.append(
{
"name": "handler.py has run()",
"passed": True,
"message": "handler.py contains run() function",
}
)
else:
checks.append(
{
"name": "handler.py has run()",
"passed": False,
"message": "handler.py missing run() function (required for Tier 2+)",
}
)
# Check 4: If apps/ exists, it must have modules/ and handlers/ subdirs
if has_apps:
apps_path = path / "apps"
has_modules = (apps_path / "modules").exists()
has_handlers = (apps_path / "handlers").exists()
if has_modules and has_handlers:
checks.append(
{
"name": "apps/ 3-layer structure",
"passed": True,
"message": "apps/ has both modules/ and handlers/ (Tier 3)",
}
)
else:
missing = []
if not has_modules:
missing.append("modules/")
if not has_handlers:
missing.append("handlers/")
checks.append(
{
"name": "apps/ 3-layer structure",
"passed": False,
"message": f"apps/ missing: {', '.join(missing)} (required for Tier 3)",
}
)
# Check 5: No stray files outside the structure
stray_items = []
try:
for item in path.iterdir():
if item.name not in VALID_ROOT_ITEMS:
stray_items.append(item.name)
except OSError:
pass
if stray_items:
checks.append(
{
"name": "No stray files",
"passed": False,
"message": f"Unexpected items in skill root: {', '.join(sorted(stray_items))}",
}
)
else:
checks.append(
{
"name": "No stray files",
"passed": True,
"message": "All items match valid skill structure",
}
)
# Determine detected tier for reporting
if has_apps:
tier = "Tier 3 (3-layer)"
elif has_handler:
tier = "Tier 2 (executable)"
else:
tier = "Tier 1 (minimal)"
checks.append(
{
"name": "Tier detection",
"passed": True,
"message": f"Detected: {tier}",
}
)
# Calculate score
passed_count = sum(1 for c in checks if c["passed"])
total = len(checks)
score = (passed_count / total * 100) if total > 0 else 0.0
return {
"passed": score >= 75,
"checks": checks,
"score": score,
"standard": "SKILL_STRUCTURE",
}
@@ -0,0 +1,74 @@
"""
Skill Structure Standards Content Handler
Provides formatted directory structure standards content.
Module orchestrates, handler implements.
"""
# =================== META ====================
# Name: skill_structure_content.py
# Description: Skill Structure Standards Content Handler
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
def get_skill_structure_standards() -> str:
"""Return formatted skill structure standards content with Rich markup
Returns:
str: Formatted standards text with Rich styling
"""
lines = [
"[bold cyan]CORE PRINCIPLE:[/bold cyan]",
" Skills follow a tiered structure based on complexity",
" Simple skills need only a SKILL.md; complex skills use the 3-layer pattern",
"",
"[bold cyan]TIER 1 - MINIMAL (documentation only):[/bold cyan]",
"",
" [dim]skill_name/[/dim]",
" [dim] SKILL.md[/dim]",
"",
" Use when: skill is a prompt, reference, or static content",
"",
"[bold cyan]TIER 2 - EXECUTABLE (single handler):[/bold cyan]",
"",
" [dim]skill_name/[/dim]",
" [dim] SKILL.md[/dim]",
" [dim] handler.py[/dim]",
"",
" Use when: skill has executable logic but is self-contained",
"",
"[bold cyan]TIER 3 - FULL (3-layer architecture):[/bold cyan]",
"",
" [dim]skill_name/[/dim]",
" [dim] SKILL.md[/dim]",
" [dim] apps/[/dim]",
" [dim] modules/[/dim]",
" [dim] handlers/[/dim]",
"",
" Use when: skill has complex logic requiring orchestration",
"",
"[yellow]CHECKS PERFORMED:[/yellow]",
"",
" 1. [bold]Skill directory exists[/bold] and is a directory",
" 2. [bold]SKILL.md exists[/bold] (required for all tiers)",
" 3. [bold]handler.py has run()[/bold] if handler.py exists",
" 4. [bold]apps/ has modules/ and handlers/[/bold] if apps/ exists",
" 5. [bold]No stray files[/bold] outside the valid structure",
"",
"[yellow]VALID FILES IN SKILL ROOT:[/yellow]",
" SKILL.md, handler.py, __init__.py, apps/, tests/, docs/,",
" requirements.txt, .gitignore",
"",
"[yellow]WARNINGS:[/yellow]",
" - Tier 3 without modules/ or handlers/ = [red]incomplete 3-layer[/red]",
" - handler.py without run() = [red]broken contract[/red]",
" - Stray files = [yellow]possible misplaced code[/yellow]",
"",
"[bold cyan]REFERENCE:[/bold cyan]",
" [dim]See: seedgo standards pack (skills)[/dim]",
]
return "\n".join(lines)
@@ -0,0 +1,6 @@
"""
Skills Modules - Orchestration Layer
Modules coordinate workflows by calling handlers.
Each module implements handle_command() for auto-discovery.
"""
@@ -0,0 +1,102 @@
"""
Skill Format Standards Module
Provides SKILL.md format compliance standards for AIPass skills.
Run directly or via: seedgo skill_format
"""
# =================== META ====================
# Name: skill_format_standard.py
# Description: Skill Format Standards Module
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import sys
from pathlib import Path
from typing import List
from aipass.prax import logger
from aipass.seedgo.apps.standards.skills.handlers.standards.skill_format_content import get_skill_format_standards
from aipass.cli import console, header
def print_introspection():
"""Display module info and connected handlers"""
console.print()
console.print("[bold cyan]Skill Format Standards Module[/bold cyan]")
console.print()
console.print("[yellow]Connected Handlers:[/yellow]")
console.print()
handlers_dir = Path(__file__).parent.parent / "handlers" / "standards"
if handlers_dir.exists():
console.print(" [cyan]handlers/standards/[/cyan]")
console.print(" [dim]- skill_format_content.py[/dim]")
console.print(" [dim]- skill_format_check.py[/dim]")
console.print()
console.print("[dim]Run 'python3 skill_format_standard.py --help' for usage[/dim]")
console.print()
def print_help():
"""Print drone-compliant help output"""
console.print()
console.print("[bold cyan]Skill Format Standards Module[/bold cyan]")
console.print("Validates SKILL.md format compliance")
console.print()
console.print("[yellow]COMMANDS:[/yellow]")
console.print(" Commands: skill_format, --help")
console.print()
console.print(" [cyan]skill_format[/cyan] - Display skill format standards")
console.print()
console.print("[yellow]USAGE:[/yellow]")
console.print(" seedgo skill_format")
console.print(" python3 skill_format_standard.py")
console.print(" python3 skill_format_standard.py --help")
console.print()
console.print("[yellow]REFERENCE:[/yellow]")
console.print(" See: seedgo standards pack (skills)")
console.print()
def handle_command(command: str, args: List[str]) -> bool:
"""Handle 'skill_format' command"""
if command != "skill_format":
return False
print_standard()
return True
def print_standard():
"""Print skill format standards - orchestrates handler call"""
console.print()
header("Skill Format Standards")
console.print()
console.print(get_skill_format_standards())
console.print()
console.print("-" * 70)
console.print()
if __name__ == "__main__":
if len(sys.argv) == 1:
print_introspection()
sys.exit(0)
if sys.argv[1] in ['--help', '-h', 'help']:
print_help()
sys.exit(0)
logger.info("Prax logger connected to skill_format_standard")
print_standard()
@@ -0,0 +1,102 @@
"""
Skill Handler Standards Module
Provides handler contract compliance standards for AIPass skills.
Run directly or via: seedgo skill_handler
"""
# =================== META ====================
# Name: skill_handler_standard.py
# Description: Skill Handler Standards Module
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import sys
from pathlib import Path
from typing import List
from aipass.prax import logger
from aipass.seedgo.apps.standards.skills.handlers.standards.skill_handler_content import get_skill_handler_standards
from aipass.cli import console, header
def print_introspection():
"""Display module info and connected handlers"""
console.print()
console.print("[bold cyan]Skill Handler Standards Module[/bold cyan]")
console.print()
console.print("[yellow]Connected Handlers:[/yellow]")
console.print()
handlers_dir = Path(__file__).parent.parent / "handlers" / "standards"
if handlers_dir.exists():
console.print(" [cyan]handlers/standards/[/cyan]")
console.print(" [dim]- skill_handler_content.py[/dim]")
console.print(" [dim]- skill_handler_check.py[/dim]")
console.print()
console.print("[dim]Run 'python3 skill_handler_standard.py --help' for usage[/dim]")
console.print()
def print_help():
"""Print drone-compliant help output"""
console.print()
console.print("[bold cyan]Skill Handler Standards Module[/bold cyan]")
console.print("Validates skill handler contract compliance")
console.print()
console.print("[yellow]COMMANDS:[/yellow]")
console.print(" Commands: skill_handler, --help")
console.print()
console.print(" [cyan]skill_handler[/cyan] - Display skill handler standards")
console.print()
console.print("[yellow]USAGE:[/yellow]")
console.print(" seedgo skill_handler")
console.print(" python3 skill_handler_standard.py")
console.print(" python3 skill_handler_standard.py --help")
console.print()
console.print("[yellow]REFERENCE:[/yellow]")
console.print(" See: seedgo standards pack (skills)")
console.print()
def handle_command(command: str, args: List[str]) -> bool:
"""Handle 'skill_handler' command"""
if command != "skill_handler":
return False
print_standard()
return True
def print_standard():
"""Print skill handler standards - orchestrates handler call"""
console.print()
header("Skill Handler Standards")
console.print()
console.print(get_skill_handler_standards())
console.print()
console.print("-" * 70)
console.print()
if __name__ == "__main__":
if len(sys.argv) == 1:
print_introspection()
sys.exit(0)
if sys.argv[1] in ['--help', '-h', 'help']:
print_help()
sys.exit(0)
logger.info("Prax logger connected to skill_handler_standard")
print_standard()
@@ -0,0 +1,102 @@
"""
Skill Structure Standards Module
Provides directory structure compliance standards for AIPass skills.
Run directly or via: seedgo skill_structure
"""
# =================== META ====================
# Name: skill_structure_standard.py
# Description: Skill Structure Standards Module
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import sys
from pathlib import Path
from typing import List
from aipass.prax import logger
from aipass.seedgo.apps.standards.skills.handlers.standards.skill_structure_content import get_skill_structure_standards
from aipass.cli import console, header
def print_introspection():
"""Display module info and connected handlers"""
console.print()
console.print("[bold cyan]Skill Structure Standards Module[/bold cyan]")
console.print()
console.print("[yellow]Connected Handlers:[/yellow]")
console.print()
handlers_dir = Path(__file__).parent.parent / "handlers" / "standards"
if handlers_dir.exists():
console.print(" [cyan]handlers/standards/[/cyan]")
console.print(" [dim]- skill_structure_content.py[/dim]")
console.print(" [dim]- skill_structure_check.py[/dim]")
console.print()
console.print("[dim]Run 'python3 skill_structure_standard.py --help' for usage[/dim]")
console.print()
def print_help():
"""Print drone-compliant help output"""
console.print()
console.print("[bold cyan]Skill Structure Standards Module[/bold cyan]")
console.print("Validates skill directory structure compliance")
console.print()
console.print("[yellow]COMMANDS:[/yellow]")
console.print(" Commands: skill_structure, --help")
console.print()
console.print(" [cyan]skill_structure[/cyan] - Display skill structure standards")
console.print()
console.print("[yellow]USAGE:[/yellow]")
console.print(" seedgo skill_structure")
console.print(" python3 skill_structure_standard.py")
console.print(" python3 skill_structure_standard.py --help")
console.print()
console.print("[yellow]REFERENCE:[/yellow]")
console.print(" See: seedgo standards pack (skills)")
console.print()
def handle_command(command: str, args: List[str]) -> bool:
"""Handle 'skill_structure' command"""
if command != "skill_structure":
return False
print_standard()
return True
def print_standard():
"""Print skill structure standards - orchestrates handler call"""
console.print()
header("Skill Structure Standards")
console.print()
console.print(get_skill_structure_standards())
console.print()
console.print("-" * 70)
console.print()
if __name__ == "__main__":
if len(sys.argv) == 1:
print_introspection()
sys.exit(0)
if sys.argv[1] in ['--help', '-h', 'help']:
print_help()
sys.exit(0)
logger.info("Prax logger connected to skill_structure_standard")
print_standard()
@@ -0,0 +1,109 @@
"""
Skills Standards Pack - Entry Point
Auto-discovery architecture:
- Scans modules/ directory for .py files with handle_command()
- Routes commands to discovered modules automatically
- No manual imports or routing needed
"""
# =================== META ====================
# Name: pack_entry.py
# Description: Skills Standards Pack - Entry Point
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import sys
import importlib.util
from pathlib import Path
from typing import List, Any
from aipass.prax import logger
from aipass.cli import console, header
# =============================================================================
# MODULE DISCOVERY
# =============================================================================
MODULES_DIR = Path(__file__).parent / "modules"
VERSION = "1.0.0"
def discover_modules() -> List[Any]:
"""Auto-discover modules in modules/ directory."""
modules = []
if not MODULES_DIR.exists():
return modules
for file_path in sorted(MODULES_DIR.glob("*.py")):
if file_path.name.startswith("_"):
continue
try:
spec = importlib.util.spec_from_file_location(file_path.stem, file_path)
if spec is None or spec.loader is None:
continue
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if hasattr(module, "handle_command"):
modules.append(module)
except Exception as e:
logger.error(f"[SKILLS] Failed to load module {file_path.stem}: {e}")
return modules
def route_command(command: str, args: List[str], modules: List[Any]) -> bool:
"""Route command to appropriate module."""
for module in modules:
try:
if module.handle_command(command, args):
return True
except Exception as e:
logger.error(f"[SKILLS] Module {module.__name__} error: {e}")
return False
# =============================================================================
# MAIN ENTRY POINT
# =============================================================================
def main():
"""Main entry point - routes commands or shows help."""
modules = discover_modules()
args = sys.argv[1:]
if not args or args[0] in ["--help", "-h", "help"]:
header("Skills Standards Pack")
console.print()
console.print(f" {len(modules)} modules discovered")
console.print()
for module in modules:
name = getattr(module, "__name__", "unknown").split(".")[-1]
desc = (module.__doc__ or "").strip().split("\n")[0] if module.__doc__ else "No description"
console.print(f" {name:20} {desc}")
console.print()
return 0
if args[0] in ["--version", "-V"]:
console.print(f"skills-standards v{VERSION}")
return 0
command = args[0]
remaining = args[1:] if len(args) > 1 else []
if route_command(command, remaining, modules):
return 0
console.print(f"Unknown command: {command}")
console.print("Run 'skills --help' for available commands")
return 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1 @@
# Plugins package - Pluggable components for skills capabilities
+119
View File
@@ -0,0 +1,119 @@
# Skills
Capability framework for AI agents in AIPass. Skills are discoverable, validatable, and executable units of capability that any AI agent can use.
## Three Tiers
### 1. Markdown Only
A `SKILL.md` file with instructions. The AI reads the instructions and follows them. No code required.
```
my-skill/
SKILL.md
```
### 2. With Handler
A `SKILL.md` plus a `handler.py` that the system can execute programmatically.
```
my-skill/
SKILL.md
handler.py
```
### 3. Full 3-Layer
A `SKILL.md` plus a full AIPass 3-layer app structure for complex skills.
```
my-skill/
SKILL.md
apps/
__init__.py
modules/
__init__.py
handlers/
__init__.py
```
## Creating a Skill
```bash
# Markdown only (default)
drone @skills create my-skill
# With handler
drone @skills create my-skill --with-handler
# Full 3-layer
drone @skills create my-skill --full
```
Skills are created in `.aipass/skills/` in the current project directory.
## Running a Skill
```bash
# Run a handler-based skill
drone @skills run my-skill action-name key=value
# Run a markdown skill (displays instructions)
drone @skills run my-skill
# List all available skills
drone @skills list
# Get details about a skill
drone @skills info my-skill
# Check requirements
drone @skills validate my-skill
```
## SKILL.md Format
```yaml
---
name: skill-name
description: One-line description
version: 1.0.0
tags: [category1, category2]
requires:
pip: [] # Python packages needed
bins: [] # CLI tools needed
config: [] # Env vars / config keys needed
has_handler: false
---
# Skill Name
## What This Does
...
## Steps
...
```
## Search Paths
Skills are discovered in this order (first match wins for same name):
1. **Project**: `.aipass/skills/` in the current working directory
2. **Global**: `~/.aipass/skills/` in the user's home directory
3. **Built-in**: `src/skills/catalog/` in the AIPass codebase
## Directory Structure
```
src/skills/
apps/
skills.py # Entry point (handle_command)
modules/
discovery.py # Find skills across search paths
loader.py # Load SKILL.md + handlers
runner.py # Execute skills
creator.py # Scaffold new skills
handlers/
registry.py # Skill registry management
validator.py # Check requirements
template.py # Skill templates
catalog/ # Built-in skills
templates/ # Skill creation templates
.trinity/ # Branch identity and memory
tests/ # Test suite
```
+13
View File
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - Skills package root
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Package root for the Skills system
# =============================================
+13
View File
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - Skills apps package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Apps layer: entry points and command routing
# =============================================
+13
View File
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - Skills handlers package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/handlers
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Handlers layer: implementation details (returns dicts, NEVER prints)
# =============================================
+74
View File
@@ -0,0 +1,74 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: registry.py - Skill registry management
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/handlers
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Handler layer: returns dicts, NEVER prints
# - Manages discovered skills cache
# =============================================
from pathlib import Path
def build_registry(search_paths, discover_fn):
"""Discover and cache all skills from search paths.
Args:
search_paths: List of (path, source_label) tuples to scan.
discover_fn: Callable that takes a path and source label,
returns list of skill dicts.
Returns:
list[dict]: All discovered skills across all search paths.
Each dict has: name, description, path, has_handler, source, tags.
"""
registry = []
seen_names = set()
for search_path, source_label in search_paths:
path = Path(search_path)
if not path.exists():
continue
skills = discover_fn(path, source_label)
for skill in skills:
# First match wins for same name
if skill["name"] not in seen_names:
seen_names.add(skill["name"])
registry.append(skill)
return registry
def get_skill(name, registry):
"""Look up a skill by name in the registry.
Args:
name: Skill name to find.
registry: List of skill dicts from build_registry.
Returns:
dict or None: The matching skill dict, or None if not found.
"""
for skill in registry:
if skill["name"] == name:
return skill
return None
def get_skill_names(registry):
"""Get all skill names from the registry.
Args:
registry: List of skill dicts from build_registry.
Returns:
list[str]: Sorted list of skill names.
"""
return sorted(skill["name"] for skill in registry)
+105
View File
@@ -0,0 +1,105 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: template.py - Skill template management
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/handlers
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Handler layer: returns dicts, NEVER prints
# - Resolves template paths and copies template content
# =============================================
import shutil
from pathlib import Path
# Template directory lives at src/skills/templates/
TEMPLATES_DIR = Path(__file__).resolve().parent.parent.parent / "templates"
VALID_TYPES = ("markdown_only", "with_handler", "full")
def get_template(template_type):
"""Get the path to a template directory.
Args:
template_type: One of "markdown_only", "with_handler", "full".
Returns:
dict: {"success": bool, "path": Path|None, "error": str|None}
"""
if template_type not in VALID_TYPES:
return {
"success": False,
"path": None,
"error": f"Unknown template type: {template_type}. Valid types: {', '.join(VALID_TYPES)}",
}
template_path = TEMPLATES_DIR / template_type
if not template_path.exists():
return {
"success": False,
"path": None,
"error": f"Template directory not found: {template_path}",
}
return {"success": True, "path": template_path, "error": None}
def copy_template(template_path, target_path, skill_name):
"""Copy a template directory to a target location, replacing placeholders.
Args:
template_path: Path to the source template directory.
target_path: Path to the destination directory for the new skill.
skill_name: Name to replace {{SKILL_NAME}} with in all files.
Returns:
dict: {"success": bool, "created_files": list[str], "error": str|None}
"""
target = Path(target_path)
if target.exists():
return {
"success": False,
"created_files": [],
"error": f"Target directory already exists: {target}",
}
try:
# Copy the entire template tree
shutil.copytree(str(template_path), str(target))
# Replace placeholders in all files
created_files = []
for file_path in target.rglob("*"):
if file_path.is_file():
created_files.append(str(file_path.relative_to(target)))
try:
content = file_path.read_text(encoding="utf-8")
if "{{SKILL_NAME}}" in content:
content = content.replace("{{SKILL_NAME}}", skill_name)
file_path.write_text(content, encoding="utf-8")
except UnicodeDecodeError:
# Skip binary files
pass
return {
"success": True,
"created_files": sorted(created_files),
"error": None,
}
except Exception as e:
# Clean up on failure
if target.exists():
shutil.rmtree(str(target))
return {
"success": False,
"created_files": [],
"error": f"Failed to create skill: {e}",
}
+109
View File
@@ -0,0 +1,109 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: validator.py - Check skill requirements
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/handlers
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Handler layer: returns dicts, NEVER prints
# - Checks pip packages, CLI bins, and config/env vars
# =============================================
import importlib.util
import os
import shutil
def validate_skill(skill_metadata):
"""Check if a skill's requirements are met.
Args:
skill_metadata: Dict with 'requires' key containing:
- pip: list of Python package names
- bins: list of CLI tool names
- config: list of env var / config key names
Returns:
dict: {
"valid": bool,
"missing_pip": list[str],
"missing_bins": list[str],
"missing_config": list[str]
}
"""
requires = skill_metadata.get("requires", {})
pip_packages = requires.get("pip", []) or []
bins = requires.get("bins", []) or []
config_keys = requires.get("config", []) or []
missing_pip = _check_pip(pip_packages)
missing_bins = _check_bins(bins)
missing_config = _check_config(config_keys)
valid = not (missing_pip or missing_bins or missing_config)
return {
"valid": valid,
"missing_pip": missing_pip,
"missing_bins": missing_bins,
"missing_config": missing_config,
}
def _check_pip(packages):
"""Check which pip packages are missing.
Args:
packages: List of Python package names.
Returns:
list[str]: Names of packages that are not installed.
"""
missing = []
for pkg in packages:
# Normalize package name for import (e.g., some-pkg -> some_pkg)
import_name = pkg.replace("-", "_")
try:
spec = importlib.util.find_spec(import_name)
if spec is None:
missing.append(pkg)
except (ModuleNotFoundError, ValueError):
missing.append(pkg)
return missing
def _check_bins(bins):
"""Check which CLI binaries are missing from PATH.
Args:
bins: List of CLI tool names.
Returns:
list[str]: Names of binaries not found in PATH.
"""
missing = []
for binary in bins:
if shutil.which(binary) is None:
missing.append(binary)
return missing
def _check_config(config_keys):
"""Check which config/env vars are missing.
Args:
config_keys: List of environment variable names.
Returns:
list[str]: Names of env vars that are not set.
"""
missing = []
for key in config_keys:
if os.environ.get(key) is None:
missing.append(key)
return missing
+13
View File
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - Skills modules package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Modules layer: business logic orchestration (can print)
# =============================================
+107
View File
@@ -0,0 +1,107 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: creator.py - Scaffold new skills from templates
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Module layer: orchestration (can print)
# - Creates new skills from template directories
# =============================================
"""Skill creator module.
Scaffolds new skills from templates into a target location.
Supports three tiers: markdown_only, with_handler, full.
"""
from pathlib import Path
from ..handlers.template import copy_template, get_template
def create_skill(name, template_type="markdown_only", target_dir=None):
"""Create a new skill from a template.
Args:
name: Name for the new skill (used as directory name and placeholder).
template_type: Template tier - "markdown_only", "with_handler", or "full".
target_dir: Directory to create the skill in. Defaults to
.aipass/skills/ in the current working directory.
Returns:
dict: {"success": bool, "path": str|None, "files": list[str], "error": str|None}
"""
# Validate skill name
if not name:
return {
"success": False,
"path": None,
"files": [],
"error": "Skill name is required.",
}
if not _is_valid_name(name):
return {
"success": False,
"path": None,
"files": [],
"error": f"Invalid skill name: '{name}'. Use lowercase letters, numbers, and hyphens only.",
}
# Resolve template
template_result = get_template(template_type)
if not template_result["success"]:
return {
"success": False,
"path": None,
"files": [],
"error": template_result["error"],
}
# Determine target directory
if target_dir is None:
target_dir = Path.cwd() / ".aipass" / "skills"
target_path = Path(target_dir) / name
# Ensure parent directory exists
target_path.parent.mkdir(parents=True, exist_ok=True)
# Copy template
result = copy_template(template_result["path"], target_path, name)
if result["success"]:
print(f" Created skill '{name}' at {target_path}")
print(f" Template: {template_type}")
print(f" Files: {len(result['created_files'])}")
for f in result["created_files"]:
print(f" - {f}")
return {
"success": result["success"],
"path": str(target_path) if result["success"] else None,
"files": result["created_files"],
"error": result["error"],
}
def _is_valid_name(name):
"""Check if a skill name is valid.
Valid names contain only lowercase letters, numbers, and hyphens.
Must start with a letter.
Args:
name: The skill name to validate.
Returns:
bool: True if valid.
"""
if not name or not name[0].isalpha():
return False
return all(c.isalnum() or c == "-" for c in name) and name == name.lower()
+264
View File
@@ -0,0 +1,264 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: discovery.py - Find skills across search paths
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Module layer: orchestration (can print)
# - Discovers skills by scanning for SKILL.md files
# - Falls back to simple frontmatter parser if yaml unavailable
# =============================================
"""Skill discovery module.
Scans search paths for directories containing SKILL.md files and
extracts metadata from YAML frontmatter.
"""
from pathlib import Path
# Try yaml, fall back to simple parser
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
def get_search_paths():
"""Return the ordered list of skill search paths.
Search order (first match wins for same name):
1. Current project: .aipass/skills/
2. Global user: ~/.aipass/skills/
3. Built-in: src/skills/catalog/
Returns:
list[tuple[Path, str]]: List of (path, source_label) tuples.
"""
paths = []
# 1. Current project
project_path = Path.cwd() / ".aipass" / "skills"
paths.append((project_path, "project"))
# 2. Global user
global_path = Path.home() / ".aipass" / "skills"
paths.append((global_path, "global"))
# 3. Built-in catalog
builtin_path = Path(__file__).resolve().parent.parent.parent / "catalog"
paths.append((builtin_path, "builtin"))
return paths
def discover_skills_in_path(search_path, source_label):
"""Scan a directory for skill directories containing SKILL.md.
Args:
search_path: Path to scan for skill directories.
source_label: Label for the source (project, global, builtin).
Returns:
list[dict]: List of skill dicts with keys:
name, description, path, has_handler, source, tags.
"""
path = Path(search_path)
if not path.exists() or not path.is_dir():
return []
skills = []
for item in sorted(path.iterdir()):
if not item.is_dir():
continue
skill_md = item / "SKILL.md"
if not skill_md.exists():
continue
metadata = parse_frontmatter(skill_md)
if metadata is None:
continue
skills.append({
"name": metadata.get("name", item.name),
"description": metadata.get("description", "No description"),
"path": item,
"has_handler": metadata.get("has_handler", False),
"source": source_label,
"tags": metadata.get("tags", []),
})
return skills
def discover_all():
"""Discover all skills across all search paths.
Returns:
list[dict]: All discovered skills, deduplicated by name
(first match wins).
"""
from ..handlers.registry import build_registry
search_paths = get_search_paths()
return build_registry(search_paths, discover_skills_in_path)
def parse_frontmatter(skill_md_path):
"""Parse YAML frontmatter from a SKILL.md file.
Frontmatter must be delimited by '---' lines at the top of the file.
Args:
skill_md_path: Path to the SKILL.md file.
Returns:
dict or None: Parsed frontmatter metadata, or None if invalid.
"""
try:
content = Path(skill_md_path).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return None
return _extract_frontmatter(content)
def _extract_frontmatter(content):
"""Extract and parse YAML frontmatter from file content.
Args:
content: Full text content of a SKILL.md file.
Returns:
dict or None: Parsed frontmatter, or None if not found.
"""
lines = content.strip().splitlines()
if not lines or lines[0].strip() != "---":
return None
# Find closing ---
end_idx = None
for i in range(1, len(lines)):
if lines[i].strip() == "---":
end_idx = i
break
if end_idx is None:
return None
frontmatter_text = "\n".join(lines[1:end_idx])
if HAS_YAML:
try:
return yaml.safe_load(frontmatter_text)
except yaml.YAMLError:
return _simple_frontmatter_parse(frontmatter_text)
else:
return _simple_frontmatter_parse(frontmatter_text)
def _simple_frontmatter_parse(text):
"""Simple YAML-like frontmatter parser (no yaml dependency).
Handles flat key: value pairs, simple lists with [] syntax,
and nested keys one level deep (e.g., requires.pip).
Args:
text: Raw frontmatter text (without --- delimiters).
Returns:
dict: Parsed key-value pairs.
"""
result = {}
current_key = None
current_list = None
for line in text.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
# Check for list item under a nested key
if stripped.startswith("- ") and current_list is not None:
value = stripped[2:].strip().strip("'\"")
if value:
current_list.append(value)
continue
# Check for key: value
if ":" in stripped:
# Reset list tracking
current_list = None
colon_idx = stripped.index(":")
key = stripped[:colon_idx].strip()
value = stripped[colon_idx + 1:].strip()
# Detect indentation for nested keys
indent = len(line) - len(line.lstrip())
if indent > 0 and current_key is not None:
# Nested key (e.g., pip: [] under requires:)
if not isinstance(result.get(current_key), dict):
result[current_key] = {}
parsed_value = _parse_simple_value(value)
result[current_key][key] = parsed_value
if isinstance(parsed_value, list):
current_list = parsed_value
# Store reference so appending works
result[current_key][key] = current_list
else:
# Top-level key
current_key = key
if value:
result[key] = _parse_simple_value(value)
else:
# Could be a nested block or empty value
result[key] = {}
return result
def _parse_simple_value(value):
"""Parse a simple YAML value string.
Args:
value: Raw value string.
Returns:
Parsed value (str, bool, int, float, or list).
"""
# Empty brackets = empty list
if value == "[]":
return []
# Inline list: [item1, item2]
if value.startswith("[") and value.endswith("]"):
inner = value[1:-1].strip()
if not inner:
return []
items = [item.strip().strip("'\"") for item in inner.split(",")]
return [item for item in items if item]
# Boolean
if value.lower() == "true":
return True
if value.lower() == "false":
return False
# Numeric
try:
if "." in value:
return float(value)
return int(value)
except ValueError:
pass
# String (strip quotes)
return value.strip("'\"")
+167
View File
@@ -0,0 +1,167 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: loader.py - Load SKILL.md and handlers
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Module layer: orchestration (can print)
# - Loads skill metadata, body, and optional handler module
# =============================================
"""Skill loader module.
Loads a skill by name: finds it via discovery, parses the full SKILL.md
(frontmatter + body), and imports the handler if present.
"""
import importlib.util
import sys
from pathlib import Path
from .discovery import discover_all, parse_frontmatter
def load_skill(name):
"""Load a skill by name.
Steps:
1. Find skill via discovery
2. Parse full SKILL.md (frontmatter + body)
3. If has_handler is true, import handler.py from skill directory
4. Return loaded skill dict
Args:
name: The skill name to load.
Returns:
dict: {
"success": bool,
"metadata": dict or None,
"body": str or None,
"handler": module or None,
"path": Path or None,
"error": str or None
}
"""
# Step 1: Find via discovery
registry = discover_all()
skill_entry = None
for skill in registry:
if skill["name"] == name:
skill_entry = skill
break
if skill_entry is None:
return {
"success": False,
"metadata": None,
"body": None,
"handler": None,
"path": None,
"error": f"Skill not found: {name}",
}
skill_path = Path(skill_entry["path"])
skill_md = skill_path / "SKILL.md"
# Step 2: Parse full SKILL.md
metadata, body = _parse_full_skill_md(skill_md)
if metadata is None:
return {
"success": False,
"metadata": None,
"body": None,
"handler": None,
"path": skill_path,
"error": f"Failed to parse SKILL.md at {skill_md}",
}
# Step 3: Import handler if present
handler = None
if metadata.get("has_handler", False):
handler = _load_handler(skill_path, name)
if handler is None:
print(f" Warning: has_handler is true but handler.py not found at {skill_path}")
return {
"success": True,
"metadata": metadata,
"body": body,
"handler": handler,
"path": skill_path,
"error": None,
}
def _parse_full_skill_md(skill_md_path):
"""Parse a SKILL.md file into frontmatter metadata and body text.
Args:
skill_md_path: Path to the SKILL.md file.
Returns:
tuple: (metadata_dict, body_string) or (None, None) on failure.
"""
try:
content = Path(skill_md_path).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return None, None
lines = content.strip().splitlines()
if not lines or lines[0].strip() != "---":
return None, None
# Find closing ---
end_idx = None
for i in range(1, len(lines)):
if lines[i].strip() == "---":
end_idx = i
break
if end_idx is None:
return None, None
# Parse frontmatter
metadata = parse_frontmatter(skill_md_path)
if metadata is None:
return None, None
# Body is everything after the closing ---
body_lines = lines[end_idx + 1:]
body = "\n".join(body_lines).strip()
return metadata, body
def _load_handler(skill_path, skill_name):
"""Dynamically import a handler.py from a skill directory.
Args:
skill_path: Path to the skill directory.
skill_name: Name of the skill (used for module naming).
Returns:
module or None: The imported handler module, or None on failure.
"""
handler_file = Path(skill_path) / "handler.py"
if not handler_file.exists():
return None
module_name = f"skills_handler_{skill_name.replace('-', '_')}"
try:
spec = importlib.util.spec_from_file_location(module_name, str(handler_file))
if spec is None or spec.loader is None:
return None
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
except Exception as exc:
print(f" Warning: Failed to load handler for {skill_name}: {exc}")
return None
+154
View File
@@ -0,0 +1,154 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: runner.py - Execute skills
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Module layer: orchestration (can print)
# - Runs skill handlers or displays markdown instructions
# =============================================
"""Skill runner module.
Executes a skill: if it has a handler, calls handler.run();
if no handler, prints the SKILL.md body for the LLM to read.
"""
from .loader import load_skill
def run_skill(name, action=None, args=None, config=None):
"""Execute a skill by name.
Args:
name: The skill name to run.
action: The action to perform (required for handler-based skills).
args: Dict of action arguments.
config: Dict of resolved config values.
Returns:
dict: {"success": bool, "output": str, "error": str|None}
"""
args = args or {}
config = config or {}
# Load the skill
loaded = load_skill(name)
if not loaded["success"]:
return {
"success": False,
"output": "",
"error": loaded["error"],
}
handler = loaded["handler"]
metadata = loaded["metadata"]
body = loaded["body"]
# If skill has a handler, call it
if handler is not None:
return _run_handler(handler, name, action, args, config)
# No handler: print the SKILL.md body (LLM reads instructions)
return _run_markdown(name, metadata, body)
def _run_handler(handler, name, action, args, config):
"""Run a skill's handler module.
Args:
handler: The imported handler module.
name: Skill name (for error messages).
action: Action to perform.
args: Dict of action arguments.
config: Dict of config values.
Returns:
dict: {"success": bool, "output": str, "error": str|None}
"""
if action is None:
# List available actions if no action specified
if hasattr(handler, "get_actions"):
try:
actions = handler.get_actions()
action_list = ", ".join(actions)
return {
"success": True,
"output": f"Available actions for {name}: {action_list}",
"error": None,
}
except Exception as exc:
return {
"success": False,
"output": "",
"error": f"Failed to list actions for {name}: {exc}",
}
return {
"success": False,
"output": "",
"error": f"No action specified for {name}. Provide an action to run.",
}
if not hasattr(handler, "run"):
return {
"success": False,
"output": "",
"error": f"Skill {name} handler has no run() function.",
}
try:
result = handler.run(action, args=args, config=config)
if isinstance(result, dict):
return {
"success": result.get("success", False),
"output": result.get("output", ""),
"error": result.get("error"),
}
# If handler returns a non-dict, wrap it
return {
"success": True,
"output": str(result),
"error": None,
}
except Exception as exc:
return {
"success": False,
"output": "",
"error": f"Skill {name} action '{action}' failed: {exc}",
}
def _run_markdown(name, metadata, body):
"""Run a markdown-only skill by returning its body content.
Args:
name: Skill name.
metadata: Skill metadata dict.
body: Markdown body text.
Returns:
dict: {"success": bool, "output": str, "error": str|None}
"""
if not body:
return {
"success": True,
"output": f"Skill '{name}' has no instructions body.",
"error": None,
}
description = metadata.get("description", "")
header = f"=== Skill: {name} ==="
if description:
header += f"\n{description}"
header += "\n"
return {
"success": True,
"output": f"{header}\n{body}",
"error": None,
}
+267
View File
@@ -0,0 +1,267 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: skills.py - Skills system entry point
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/apps
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Entry point with handle_command() for drone routing
# - Imports from modules layer only
# - Formats and prints output
# =============================================
"""Skills system entry point.
Provides handle_command(command, args) for drone routing.
Commands: list, info, run, create, validate, --help.
"""
def handle_command(command, args=None):
"""Route a skills command to the appropriate module.
Args:
command: The subcommand to execute.
args: List of additional arguments.
Returns:
bool: True if command was handled, False otherwise.
"""
args = args or []
if command in ("--help", "help", None):
_print_help()
return True
if command == "list":
return _cmd_list()
if command == "info":
if not args:
print(" Error: skill name required. Usage: skills info <name>")
return False
return _cmd_info(args[0])
if command == "run":
if not args:
print(" Error: skill name required. Usage: skills run <name> [action] [args...]")
return False
name = args[0]
action = args[1] if len(args) > 1 else None
extra_args = _parse_extra_args(args[2:]) if len(args) > 2 else {}
return _cmd_run(name, action, extra_args)
if command == "create":
if not args:
print(" Error: skill name required. Usage: skills create <name> [--with-handler|--full]")
return False
return _cmd_create(args)
if command == "validate":
if not args:
print(" Error: skill name required. Usage: skills validate <name>")
return False
return _cmd_validate(args[0])
print(f" Unknown command: {command}")
print(" Run 'skills --help' for available commands.")
return False
def _print_help():
"""Print skills help text."""
print("Skills - Capability framework for AI agents")
print()
print("Usage:")
print(" drone @skills <command> [args]")
print()
print("Commands:")
print(" list Show all discovered skills")
print(" info <name> Display SKILL.md contents")
print(" run <name> [action] [args] Execute a skill's handler")
print(" create <name> Scaffold new skill (markdown only)")
print(" create <name> --with-handler Scaffold with handler.py")
print(" create <name> --full Scaffold with full 3-layer structure")
print(" validate <name> Check if skill requirements are met")
print(" --help Show this help")
print()
print("Search paths (first match wins):")
print(" 1. .aipass/skills/ Project-local skills")
print(" 2. ~/.aipass/skills/ Global user skills")
print(" 3. src/skills/catalog/ Built-in skills")
def _cmd_list():
"""List all discovered skills."""
from .modules.discovery import discover_all
skills = discover_all()
if not skills:
print(" No skills found.")
print(" Create one with: drone @skills create <name>")
return True
print(f" Found {len(skills)} skill(s):")
print()
# Group by source
sources = {}
for skill in skills:
source = skill["source"]
if source not in sources:
sources[source] = []
sources[source].append(skill)
source_labels = {"project": "Project", "global": "Global", "builtin": "Built-in"}
for source, source_skills in sources.items():
label = source_labels.get(source, source)
print(f" [{label}]")
for skill in source_skills:
handler_tag = " [handler]" if skill["has_handler"] else ""
tags = ""
if skill.get("tags"):
tags = f" ({', '.join(skill['tags'])})"
print(f" {skill['name']:<25} {skill['description']}{handler_tag}{tags}")
print()
return True
def _cmd_info(name):
"""Display full SKILL.md contents for a skill."""
from .modules.loader import load_skill
loaded = load_skill(name)
if not loaded["success"]:
print(f" Error: {loaded['error']}")
return False
metadata = loaded["metadata"]
body = loaded["body"]
path = loaded["path"]
print(f" Skill: {metadata.get('name', name)}")
print(f" Version: {metadata.get('version', 'unknown')}")
print(f" Description: {metadata.get('description', 'No description')}")
print(f" Path: {path}")
print(f" Has Handler: {metadata.get('has_handler', False)}")
tags = metadata.get("tags", [])
if tags:
print(f" Tags: {', '.join(tags)}")
requires = metadata.get("requires", {})
if requires:
pip_pkgs = requires.get("pip", [])
bins = requires.get("bins", [])
config = requires.get("config", [])
if pip_pkgs:
print(f" Requires pip: {', '.join(pip_pkgs)}")
if bins:
print(f" Requires bins: {', '.join(bins)}")
if config:
print(f" Requires config: {', '.join(config)}")
if body:
print()
print(" --- SKILL.md Body ---")
for line in body.splitlines():
print(f" {line}")
return True
def _cmd_run(name, action, extra_args):
"""Execute a skill."""
from .modules.runner import run_skill
result = run_skill(name, action=action, args=extra_args)
if result["success"]:
if result["output"]:
for line in result["output"].splitlines():
print(f" {line}")
else:
error = result.get("error", "Unknown error")
print(f" Error: {error}")
return result["success"]
def _cmd_create(args):
"""Create a new skill from a template."""
from .modules.creator import create_skill
name = args[0]
# Determine template type from flags
template_type = "markdown_only"
if "--with-handler" in args:
template_type = "with_handler"
elif "--full" in args:
template_type = "full"
result = create_skill(name, template_type=template_type)
if not result["success"]:
print(f" Error: {result['error']}")
return False
return True
def _cmd_validate(name):
"""Validate a skill's requirements."""
from .modules.loader import load_skill
from .handlers.validator import validate_skill
loaded = load_skill(name)
if not loaded["success"]:
print(f" Error: {loaded['error']}")
return False
result = validate_skill(loaded["metadata"])
if result["valid"]:
print(f" Skill '{name}' - all requirements met.")
else:
print(f" Skill '{name}' - requirements NOT met:")
if result["missing_pip"]:
print(f" Missing pip packages: {', '.join(result['missing_pip'])}")
if result["missing_bins"]:
print(f" Missing CLI tools: {', '.join(result['missing_bins'])}")
if result["missing_config"]:
print(f" Missing config/env: {', '.join(result['missing_config'])}")
return result["valid"]
def _parse_extra_args(arg_list):
"""Parse extra arguments into a dict.
Supports key=value pairs and positional arguments.
Args:
arg_list: List of argument strings.
Returns:
dict: Parsed arguments.
"""
result = {}
positional_idx = 0
for arg in arg_list:
if "=" in arg:
key, value = arg.split("=", 1)
result[key] = value
else:
result[f"arg{positional_idx}"] = arg
positional_idx += 1
return result
View File
@@ -0,0 +1,76 @@
---
name: drone_commands
description: Execute drone commands -- the AIPass CLI interface for all module operations
version: 1.0.0
tags: [system, cli, drone, aipass]
requires:
pip: []
bins: []
config: []
has_handler: true
---
# Drone Commands Skill
Execute drone commands programmatically. Drone is the AIPass CLI router that dispatches commands to system modules.
## Available Actions
| Action | Description |
|----------|-----------------------------------------------------|
| `run` | Execute an arbitrary drone command string |
| `list` | List all available drone modules (`drone systems`) |
| `help` | Get help for a specific module (`drone @module --help`) |
## Usage
```bash
drone @skills run drone_commands run --args '{"command": "drone @ai_mail inbox"}'
drone @skills run drone_commands list
drone @skills run drone_commands help --args '{"module": "ai_mail"}'
```
## How Drone Routing Works
Drone uses `@module` syntax to route commands to the correct system module:
```
drone @ai_mail inbox -> routes to ai_mail module
drone @skills list -> routes to skills module
drone @devpulse dashboard -> routes to devpulse module
drone commons feed -> special case (no @ prefix)
drone systems -> lists all registered modules
```
## Architecture
This skill follows the AIPass 3-layer pattern:
```
drone_commands/
SKILL.md # This file
handler.py # Top-level handler (delegates to apps/)
apps/
modules/
command_runner.py # Orchestrates drone command execution
handlers/
executor.py # Runs commands via subprocess
parser.py # Parses drone output
```
## Output Format
All actions return structured dicts:
```python
{"success": True, "output": "...", "error": None}
```
The `run` action returns the full stdout/stderr from the drone command.
## Notes
- Commands execute in the AIPASS_ROOT directory by default
- Timeout defaults to 30 seconds (configurable)
- Never runs commands that modify system state without explicit action
- All output is captured, never printed directly
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - drone_commands apps package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/drone_commands/apps
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Apps layer entry point
# =============================================
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - drone_commands handlers package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/drone_commands/apps/handlers
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Handlers layer: returns dicts, NEVER prints
# =============================================
@@ -0,0 +1,104 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: executor.py - Runs drone commands via subprocess
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/drone_commands/apps/handlers
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Handlers layer: returns dicts, NEVER prints
# - stdlib only (no external deps)
# - Graceful error handling
# =============================================
"""
Executor handler for drone commands.
Runs shell commands via subprocess and captures output.
Never prints -- always returns structured dicts.
"""
import os
import subprocess
from pathlib import Path
AIPASS_ROOT = Path(os.environ.get("AIPASS_ROOT", str(Path.home())))
DEFAULT_TIMEOUT = 30
def execute(command, cwd=None, timeout=None):
"""Run a command via subprocess and capture output.
Args:
command: The command string to execute.
cwd: Working directory for the command. Defaults to AIPASS_ROOT.
timeout: Timeout in seconds. Defaults to DEFAULT_TIMEOUT.
Returns:
{
"success": bool,
"stdout": str,
"stderr": str,
"returncode": int
}
"""
if cwd is None:
cwd = str(AIPASS_ROOT)
if timeout is None:
timeout = DEFAULT_TIMEOUT
# Validate command is not empty
if not command or not command.strip():
return {
"success": False,
"stdout": "",
"stderr": "Empty command",
"returncode": -1,
}
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
cwd=cwd,
timeout=timeout,
)
return {
"success": result.returncode == 0,
"stdout": result.stdout,
"stderr": result.stderr,
"returncode": result.returncode,
}
except subprocess.TimeoutExpired:
return {
"success": False,
"stdout": "",
"stderr": f"Command timed out after {timeout}s: {command}",
"returncode": -1,
}
except FileNotFoundError as exc:
return {
"success": False,
"stdout": "",
"stderr": f"File not found (bad cwd or shell?): {exc}",
"returncode": -1,
}
except OSError as exc:
return {
"success": False,
"stdout": "",
"stderr": f"OS error running command: {exc}",
"returncode": -1,
}
except Exception as exc:
return {
"success": False,
"stdout": "",
"stderr": f"Unexpected error: {exc}",
"returncode": -1,
}
@@ -0,0 +1,122 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: parser.py - Parses drone command output
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/drone_commands/apps/handlers
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Handlers layer: returns dicts, NEVER prints
# - stdlib only (no external deps)
# - Pure functions, no side effects
# =============================================
"""
Parser handler for drone command output.
Cleans up and structures raw drone output into usable data.
Never prints -- always returns structured results.
"""
import re
def parse_output(raw_output):
"""Clean up raw drone command output.
Strips ANSI escape codes, trims whitespace, and normalizes line endings.
Args:
raw_output: Raw string output from a drone command.
Returns:
str: Cleaned output string.
"""
if not raw_output:
return ""
# Strip ANSI escape sequences (color codes, cursor movements, etc.)
ansi_pattern = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
cleaned = ansi_pattern.sub("", raw_output)
# Normalize line endings
cleaned = cleaned.replace("\r\n", "\n").replace("\r", "\n")
# Strip trailing whitespace from each line, remove excess blank lines
lines = cleaned.split("\n")
lines = [line.rstrip() for line in lines]
# Collapse multiple consecutive blank lines into one
result_lines = []
prev_blank = False
for line in lines:
is_blank = len(line.strip()) == 0
if is_blank and prev_blank:
continue
result_lines.append(line)
prev_blank = is_blank
# Strip leading/trailing blank lines from result
result = "\n".join(result_lines).strip()
return result
def extract_modules(systems_output):
"""Parse `drone systems` output into a list of module names.
Expects output where each line contains a module name, possibly with
status indicators or descriptions. Extracts the module name from each
non-empty, non-header line.
Args:
systems_output: Raw output from `drone systems` command.
Returns:
list[str]: List of module name strings.
"""
if not systems_output:
return []
cleaned = parse_output(systems_output)
lines = cleaned.split("\n")
modules = []
for line in lines:
line = line.strip()
# Skip empty lines
if not line:
continue
# Skip header/separator lines (dashes, equals, common headers)
if line.startswith("---") or line.startswith("==="):
continue
if line.lower().startswith("registered") or line.lower().startswith("available"):
continue
# Extract module name -- could be first word, or prefixed with indicators
# Common formats:
# module_name - plain name
# [OK] module_name - with status
# * module_name - with bullet
# @module_name - with @ prefix
# Remove common prefixes
cleaned_line = line
cleaned_line = re.sub(r"^\[.*?\]\s*", "", cleaned_line) # [OK], [ERR], etc.
cleaned_line = re.sub(r"^[*\-+]\s*", "", cleaned_line) # bullet points
cleaned_line = cleaned_line.lstrip("@") # @ prefix
# Take first word as module name
parts = cleaned_line.split()
if parts:
module_name = parts[0].strip()
# Validate it looks like a module name (alphanumeric + underscores)
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", module_name):
modules.append(module_name)
return modules
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - drone_commands modules package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/drone_commands/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Modules layer: orchestration (can print)
# =============================================
@@ -0,0 +1,180 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: command_runner.py - Orchestrates drone command execution
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/drone_commands/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Modules layer: orchestration
# - Delegates to handlers for execution and parsing
# - Returns dicts for skill handler contract
# - stdlib only (no external deps)
# =============================================
"""
Command runner module for drone_commands skill.
Orchestrates drone command execution by coordinating between
the executor (subprocess) and parser (output cleanup) handlers.
"""
import os
import sys
# Resolve imports relative to this skill's package
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
_APPS_DIR = os.path.dirname(_THIS_DIR)
_HANDLERS_DIR = os.path.join(_APPS_DIR, "handlers")
# Add handlers to path if not already there
if _HANDLERS_DIR not in sys.path:
sys.path.insert(0, _HANDLERS_DIR)
if _APPS_DIR not in sys.path:
sys.path.insert(0, _APPS_DIR)
from handlers import executor, parser # noqa: E402
AIPASS_ROOT = os.environ.get("AIPASS_ROOT", os.path.expanduser("~"))
DRONE_BIN = os.path.join(AIPASS_ROOT, "drone")
def run_command(command_string, timeout=None):
"""Run an arbitrary drone command.
Args:
command_string: The full drone command to execute
(e.g., "drone @ai_mail inbox").
timeout: Optional timeout in seconds.
Returns:
{"success": bool, "output": str, "error": str|None}
"""
if not command_string or not command_string.strip():
return {
"success": False,
"output": "",
"error": "No command provided",
}
# Ensure command starts with "drone" if not already
cmd = command_string.strip()
if not cmd.startswith("drone"):
cmd = f"drone {cmd}"
# Execute via handler
result = executor.execute(cmd, cwd=AIPASS_ROOT, timeout=timeout)
# Parse and clean output
stdout_clean = parser.parse_output(result.get("stdout", ""))
stderr_clean = parser.parse_output(result.get("stderr", ""))
if result["success"]:
return {
"success": True,
"output": stdout_clean,
"error": None,
}
# Command failed -- include both stdout and stderr
error_parts = []
if stderr_clean:
error_parts.append(stderr_clean)
error_msg = "\n".join(error_parts) if error_parts else f"Command failed with exit code {result['returncode']}"
output = stdout_clean if stdout_clean else ""
return {
"success": False,
"output": output,
"error": error_msg,
}
def list_modules(timeout=None):
"""List all available drone modules via `drone systems`.
Args:
timeout: Optional timeout in seconds.
Returns:
{"success": bool, "output": str, "error": str|None}
"""
result = executor.execute("drone systems", cwd=AIPASS_ROOT, timeout=timeout)
if not result["success"]:
stderr_clean = parser.parse_output(result.get("stderr", ""))
return {
"success": False,
"output": "",
"error": stderr_clean or f"'drone systems' failed with exit code {result['returncode']}",
}
stdout_clean = parser.parse_output(result.get("stdout", ""))
modules = parser.extract_modules(result.get("stdout", ""))
if modules:
module_list = "\n".join(f" - {m}" for m in modules)
output = f"Registered modules ({len(modules)}):\n{module_list}"
else:
# Fallback: show raw cleaned output if parsing found nothing
output = stdout_clean if stdout_clean else "No modules found"
return {
"success": True,
"output": output,
"error": None,
}
def module_help(module_name, timeout=None):
"""Get help for a specific drone module.
Args:
module_name: The module to get help for (e.g., "ai_mail").
timeout: Optional timeout in seconds.
Returns:
{"success": bool, "output": str, "error": str|None}
"""
if not module_name or not module_name.strip():
return {
"success": False,
"output": "",
"error": "No module name provided",
}
module_name = module_name.strip().lstrip("@")
cmd = f"drone @{module_name} --help"
result = executor.execute(cmd, cwd=AIPASS_ROOT, timeout=timeout)
stdout_clean = parser.parse_output(result.get("stdout", ""))
stderr_clean = parser.parse_output(result.get("stderr", ""))
if result["success"]:
output = stdout_clean if stdout_clean else f"No help output for module '{module_name}'"
return {
"success": True,
"output": output,
"error": None,
}
# Some modules output help to stderr
if stderr_clean and ("usage" in stderr_clean.lower() or "help" in stderr_clean.lower()):
return {
"success": True,
"output": stderr_clean,
"error": None,
}
error_msg = stderr_clean or f"Failed to get help for module '{module_name}'"
return {
"success": False,
"output": stdout_clean,
"error": error_msg,
}
@@ -0,0 +1,101 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: handler.py - Drone Commands skill handler
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/drone_commands
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Top-level handler: delegates to apps/modules/
# - Returns dicts, NEVER prints
# - stdlib only (no external deps)
# - Graceful error handling
# =============================================
"""
Drone Commands skill handler.
Top-level entry point that delegates to the command_runner module
in the 3-layer apps/ structure.
Called by: drone @skills run drone_commands <action> [args]
"""
import os
import sys
# Set up import path for this skill's apps package
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
_APPS_DIR = os.path.join(_THIS_DIR, "apps")
_MODULES_DIR = os.path.join(_APPS_DIR, "modules")
if _MODULES_DIR not in sys.path:
sys.path.insert(0, _MODULES_DIR)
if _APPS_DIR not in sys.path:
sys.path.insert(0, _APPS_DIR)
from modules import command_runner # noqa: E402
def run(action, args=None, config=None):
"""Execute a drone commands action.
Args:
action: One of: run, list, help
args: Dict of action arguments:
- run: {"command": "drone @module action"}
- list: {} (no args needed)
- help: {"module": "module_name"}
config: Dict of resolved config values (unused)
Returns:
{"success": bool, "output": str, "error": str|None}
"""
args = args or {}
config = config or {}
timeout = args.get("timeout")
if timeout is not None:
try:
timeout = int(timeout)
except (ValueError, TypeError):
timeout = None
if action == "run":
command = args.get("command", "")
if not command:
return {
"success": False,
"output": "",
"error": "Missing 'command' argument. Usage: --args '{\"command\": \"drone @module action\"}'",
}
return command_runner.run_command(command, timeout=timeout)
elif action == "list":
return command_runner.list_modules(timeout=timeout)
elif action == "help":
module_name = args.get("module", "")
if not module_name:
return {
"success": False,
"output": "",
"error": "Missing 'module' argument. Usage: --args '{\"module\": \"module_name\"}'",
}
return command_runner.module_help(module_name, timeout=timeout)
else:
available = ", ".join(get_actions())
return {
"success": False,
"output": "",
"error": f"Unknown action: {action}. Available: {available}",
}
def get_actions():
"""List available actions for this skill."""
return ["run", "list", "help"]
+145
View File
@@ -0,0 +1,145 @@
---
name: github
description: "GitHub operations via gh CLI: issues, PRs, CI runs, code review, API queries."
version: 1.0.0
tags: [dev, git, ci, github]
requires:
bins: [gh]
pip: []
config: []
has_handler: false
---
# GitHub Skill
Use the `gh` CLI to interact with GitHub repositories, issues, PRs, and CI.
## When to Use
**USE this skill when:**
- Checking PR status, reviews, or merge readiness
- Viewing CI/workflow run status and logs
- Creating, closing, or commenting on issues
- Creating or merging pull requests
- Querying GitHub API for repository data
- Listing repos, releases, or collaborators
## When NOT to Use
**DON'T use this skill when:**
- Local git operations (commit, push, pull, branch) -> use `git` directly
- Non-GitHub repos (GitLab, Bitbucket, self-hosted) -> different CLIs
- Cloning repositories -> use `git clone`
- Reviewing actual code changes -> use `coding-agent` skill
- Complex multi-file diffs -> use `coding-agent` or read files directly
## Setup
```bash
# Authenticate (one-time)
gh auth login
# Verify
gh auth status
```
## Common Commands
### Pull Requests
```bash
# List PRs
gh pr list --repo owner/repo
# Check CI status
gh pr checks 55 --repo owner/repo
# View PR details
gh pr view 55 --repo owner/repo
# Create PR
gh pr create --title "feat: add feature" --body "Description"
# Merge PR
gh pr merge 55 --squash --repo owner/repo
```
### Issues
```bash
# List issues
gh issue list --repo owner/repo --state open
# Create issue
gh issue create --title "Bug: something broken" --body "Details..."
# Close issue
gh issue close 42 --repo owner/repo
```
### CI/Workflow Runs
```bash
# List recent runs
gh run list --repo owner/repo --limit 10
# View specific run
gh run view <run-id> --repo owner/repo
# View failed step logs only
gh run view <run-id> --repo owner/repo --log-failed
# Re-run failed jobs
gh run rerun <run-id> --failed --repo owner/repo
```
### API Queries
```bash
# Get PR with specific fields
gh api repos/owner/repo/pulls/55 --jq '.title, .state, .user.login'
# List all labels
gh api repos/owner/repo/labels --jq '.[].name'
# Get repo stats
gh api repos/owner/repo --jq '{stars: .stargazers_count, forks: .forks_count}'
```
## JSON Output
Most commands support `--json` for structured output with `--jq` filtering:
```bash
gh issue list --repo owner/repo --json number,title --jq '.[] | "\(.number): \(.title)"'
gh pr list --json number,title,state,mergeable --jq '.[] | select(.mergeable == "MERGEABLE")'
```
## Templates
### PR Review Summary
```bash
# Get PR overview for review
PR=55 REPO=owner/repo
echo "## PR #$PR Summary"
gh pr view $PR --repo $REPO --json title,body,author,additions,deletions,changedFiles \
--jq '"**\(.title)** by @\(.author.login)\n\n\(.body)\n\n+\(.additions) -\(.deletions) across \(.changedFiles) files"'
gh pr checks $PR --repo $REPO
```
### Issue Triage
```bash
# Quick issue triage view
gh issue list --repo owner/repo --state open --json number,title,labels,createdAt \
--jq '.[] | "[\(.number)] \(.title) - \([.labels[].name] | join(", ")) (\(.createdAt[:10]))"'
```
## Notes
- Always specify `--repo owner/repo` when not in a git directory
- Use URLs directly: `gh pr view https://github.com/owner/repo/pull/55`
- Rate limits apply; use `gh api --cache 1h` for repeated queries
+58
View File
@@ -0,0 +1,58 @@
---
name: system_status
description: Check system health -- disk usage, memory, running processes, uptime
version: 1.0.0
tags: [system, monitoring, health]
requires:
pip: []
bins: []
config: []
has_handler: true
---
# System Status Skill
Check system health metrics without leaving your workflow. Returns structured data about disk usage, memory, running processes, and system uptime.
## Available Actions
| Action | Description |
|-------------|------------------------------------------------|
| `disk` | Disk usage for the root filesystem |
| `memory` | Memory usage from /proc/meminfo (Linux) |
| `uptime` | System uptime from /proc/uptime |
| `processes` | Count of currently running processes |
| `summary` | All of the above combined into one report |
## Usage
```bash
drone @skills run system_status disk
drone @skills run system_status memory
drone @skills run system_status uptime
drone @skills run system_status processes
drone @skills run system_status summary
```
## Output Format
All actions return structured dicts:
```python
{"success": True, "output": "...", "error": None}
```
## When to Use
- Quick health check before resource-intensive operations
- Diagnosing slow performance (memory pressure, disk full)
- Monitoring system state during long-running tasks
- Getting a snapshot of system health for reports
## Notes
- All data comes from stdlib / procfs -- no external dependencies
- Memory info reads from `/proc/meminfo` (Linux only)
- Uptime reads from `/proc/uptime` (Linux only)
- Disk usage uses `shutil.disk_usage()` (cross-platform)
- Process count uses `/proc` directory listing (Linux only)
+256
View File
@@ -0,0 +1,256 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: handler.py - System Status skill handler
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/system_status
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Handler layer: returns dicts, NEVER prints
# - stdlib only (no external deps)
# - Graceful error handling on all actions
# =============================================
"""
System Status skill handler.
Provides system health information: disk usage, memory, uptime, processes.
All data sourced from stdlib and /proc (Linux).
Called by: drone @skills run system_status <action>
"""
import os
import shutil
def run(action, args=None, config=None):
"""Execute a system status action.
Args:
action: One of: disk, memory, uptime, processes, summary
args: Dict of action arguments (unused for this skill)
config: Dict of resolved config values (unused for this skill)
Returns:
{"success": bool, "output": str, "error": str|None}
"""
args = args or {}
config = config or {}
dispatch = {
"disk": _disk_usage,
"memory": _memory_info,
"uptime": _system_uptime,
"processes": _process_count,
"summary": _summary,
}
handler_fn = dispatch.get(action)
if handler_fn is None:
available = ", ".join(dispatch.keys())
return {
"success": False,
"output": "",
"error": f"Unknown action: {action}. Available: {available}",
}
try:
return handler_fn()
except Exception as exc:
return {
"success": False,
"output": "",
"error": f"Action '{action}' failed: {exc}",
}
def get_actions():
"""List available actions for this skill."""
return ["disk", "memory", "uptime", "processes", "summary"]
# ---------------------------------------------------------------------------
# Action implementations
# ---------------------------------------------------------------------------
def _format_bytes(num_bytes):
"""Format bytes into human-readable string."""
for unit in ("B", "KB", "MB", "GB", "TB"):
if abs(num_bytes) < 1024.0:
return f"{num_bytes:.1f} {unit}"
num_bytes /= 1024.0
return f"{num_bytes:.1f} PB"
def _disk_usage():
"""Get disk usage for the root filesystem."""
usage = shutil.disk_usage("/")
total = _format_bytes(usage.total)
used = _format_bytes(usage.used)
free = _format_bytes(usage.free)
percent = (usage.used / usage.total) * 100
output = (
f"Disk Usage (/)\n"
f" Total: {total}\n"
f" Used: {used} ({percent:.1f}%)\n"
f" Free: {free}"
)
return {"success": True, "output": output, "error": None}
def _memory_info():
"""Get memory info from /proc/meminfo (Linux)."""
meminfo_path = "/proc/meminfo"
if not os.path.exists(meminfo_path):
return {
"success": False,
"output": "",
"error": "/proc/meminfo not available (non-Linux system?)",
}
data = {}
with open(meminfo_path, "r", encoding="utf-8") as f:
for line in f:
parts = line.split(":")
if len(parts) == 2:
key = parts[0].strip()
# Value is in kB typically, e.g. "8045264 kB"
val_str = parts[1].strip()
# Extract numeric part
val_parts = val_str.split()
if val_parts:
try:
data[key] = int(val_parts[0])
except ValueError:
data[key] = val_str
mem_total = data.get("MemTotal", 0)
mem_free = data.get("MemFree", 0)
mem_available = data.get("MemAvailable", 0)
buffers = data.get("Buffers", 0)
cached = data.get("Cached", 0)
swap_total = data.get("SwapTotal", 0)
swap_free = data.get("SwapFree", 0)
# Values from /proc/meminfo are in kB
mem_used = mem_total - mem_available
mem_percent = (mem_used / mem_total * 100) if mem_total > 0 else 0
swap_used = swap_total - swap_free
swap_percent = (swap_used / swap_total * 100) if swap_total > 0 else 0
output = (
f"Memory\n"
f" Total: {_format_bytes(mem_total * 1024)}\n"
f" Used: {_format_bytes(mem_used * 1024)} ({mem_percent:.1f}%)\n"
f" Available: {_format_bytes(mem_available * 1024)}\n"
f" Buffers: {_format_bytes(buffers * 1024)}\n"
f" Cached: {_format_bytes(cached * 1024)}\n"
f"Swap\n"
f" Total: {_format_bytes(swap_total * 1024)}\n"
f" Used: {_format_bytes(swap_used * 1024)} ({swap_percent:.1f}%)\n"
f" Free: {_format_bytes(swap_free * 1024)}"
)
return {"success": True, "output": output, "error": None}
def _system_uptime():
"""Get system uptime from /proc/uptime (Linux)."""
uptime_path = "/proc/uptime"
if not os.path.exists(uptime_path):
return {
"success": False,
"output": "",
"error": "/proc/uptime not available (non-Linux system?)",
}
with open(uptime_path, "r", encoding="utf-8") as f:
content = f.read().strip()
parts = content.split()
if not parts:
return {
"success": False,
"output": "",
"error": "Could not parse /proc/uptime",
}
uptime_seconds = float(parts[0])
days = int(uptime_seconds // 86400)
hours = int((uptime_seconds % 86400) // 3600)
minutes = int((uptime_seconds % 3600) // 60)
seconds = int(uptime_seconds % 60)
parts_list = []
if days > 0:
parts_list.append(f"{days}d")
if hours > 0:
parts_list.append(f"{hours}h")
if minutes > 0:
parts_list.append(f"{minutes}m")
parts_list.append(f"{seconds}s")
formatted = " ".join(parts_list)
output = f"Uptime: {formatted} ({uptime_seconds:.0f} seconds total)"
return {"success": True, "output": output, "error": None}
def _process_count():
"""Count running processes via /proc directory."""
proc_path = "/proc"
if not os.path.exists(proc_path):
return {
"success": False,
"output": "",
"error": "/proc not available (non-Linux system?)",
}
count = 0
try:
for entry in os.listdir(proc_path):
# Process directories are numeric PIDs
if entry.isdigit():
count += 1
except OSError as exc:
return {
"success": False,
"output": "",
"error": f"Failed to read /proc: {exc}",
}
output = f"Running processes: {count}"
return {"success": True, "output": output, "error": None}
def _summary():
"""Combine all status checks into one report."""
sections = []
errors = []
for action_name, action_fn in [
("disk", _disk_usage),
("memory", _memory_info),
("uptime", _system_uptime),
("processes", _process_count),
]:
try:
result = action_fn()
if result["success"]:
sections.append(result["output"])
else:
errors.append(f"{action_name}: {result['error']}")
except Exception as exc:
errors.append(f"{action_name}: {exc}")
output = "\n---\n".join(sections)
if errors:
output += "\n---\nErrors:\n " + "\n ".join(errors)
return {"success": True, "output": output, "error": None}
+27
View File
@@ -0,0 +1,27 @@
---
name: {{SKILL_NAME}}
description: TODO — describe what this skill does
version: 1.0.0
tags: []
requires:
pip: []
bins: []
config: []
has_handler: true
---
# {{SKILL_NAME}}
## What This Does
TODO
## When to Use
TODO
## Steps
1. TODO
## Example
```
TODO
```
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - {{SKILL_NAME}} apps package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/{{SKILL_NAME}}/apps
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial scaffold
#
# CODE STANDARDS:
# - Apps layer entry point
# =============================================
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - {{SKILL_NAME}} handlers package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/{{SKILL_NAME}}/apps/handlers
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial scaffold
#
# CODE STANDARDS:
# - Handlers layer: returns dicts, NEVER prints
# =============================================
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - {{SKILL_NAME}} modules package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/{{SKILL_NAME}}/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial scaffold
#
# CODE STANDARDS:
# - Modules layer: orchestration (can print)
# =============================================
@@ -0,0 +1,27 @@
---
name: {{SKILL_NAME}}
description: TODO — describe what this skill does
version: 1.0.0
tags: []
requires:
pip: []
bins: []
config: []
has_handler: false
---
# {{SKILL_NAME}}
## What This Does
TODO
## When to Use
TODO
## Steps
1. TODO
## Example
```
TODO
```
@@ -0,0 +1,27 @@
---
name: {{SKILL_NAME}}
description: TODO — describe what this skill does
version: 1.0.0
tags: []
requires:
pip: []
bins: []
config: []
has_handler: true
---
# {{SKILL_NAME}}
## What This Does
TODO
## When to Use
TODO
## Steps
1. TODO
## Example
```
TODO
```
@@ -0,0 +1,30 @@
"""
{{SKILL_NAME}} skill handler
Called by: drone @skills run {{SKILL_NAME}} <action> [args]
"""
def run(action, args=None, config=None):
"""Execute a skill action.
Args:
action: What to do
args: Dict of action arguments
config: Dict of resolved config values
Returns:
{"success": bool, "output": str, "error": str|None}
"""
args = args or {}
config = config or {}
if action == "example":
return {"success": True, "output": "It works!", "error": None}
return {"success": False, "output": "", "error": f"Unknown action: {action}"}
def get_actions():
"""List available actions for this skill."""
return ["example"]
+13
View File
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - Skills tests package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/tests
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Test package for the Skills system
# =============================================
+23
View File
@@ -0,0 +1,23 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: conftest.py - Skills test configuration
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/tests
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Adds skills root to sys.path for test imports
# =============================================
"""Skills test configuration."""
import sys
from pathlib import Path
# Add skills root to path for imports
skills_root = Path(__file__).parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
+212
View File
@@ -0,0 +1,212 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: test_discovery.py - Unit tests for skills discovery
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/tests
# =============================================
"""Tests for the skills discovery module."""
import sys
import tempfile
from pathlib import Path
import pytest
# Ensure skills package is importable
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from skills.apps.modules.discovery import (
_extract_frontmatter,
_parse_simple_value,
_simple_frontmatter_parse,
discover_skills_in_path,
get_search_paths,
parse_frontmatter,
)
class TestGetSearchPaths:
def test_returns_three_paths(self):
paths = get_search_paths()
assert len(paths) == 3
def test_path_order(self):
paths = get_search_paths()
labels = [label for _, label in paths]
assert labels == ["project", "global", "builtin"]
def test_builtin_path_exists(self):
paths = get_search_paths()
builtin_path = paths[2][0]
assert builtin_path.exists()
class TestExtractFrontmatter:
def test_valid_frontmatter(self):
content = "---\nname: test\ndescription: A test skill\n---\n\n# Body"
result = _extract_frontmatter(content)
assert result is not None
assert result["name"] == "test"
assert result["description"] == "A test skill"
def test_no_frontmatter(self):
content = "# Just a markdown file\nNo frontmatter here."
result = _extract_frontmatter(content)
assert result is None
def test_unclosed_frontmatter(self):
content = "---\nname: test\nno closing delimiter"
result = _extract_frontmatter(content)
assert result is None
def test_empty_content(self):
result = _extract_frontmatter("")
assert result is None
def test_boolean_values(self):
content = "---\nname: test\nhas_handler: true\n---\n"
result = _extract_frontmatter(content)
assert result["has_handler"] is True
def test_list_values(self):
content = "---\nname: test\ntags: [dev, git, ci]\n---\n"
result = _extract_frontmatter(content)
assert result["tags"] == ["dev", "git", "ci"]
class TestSimpleFrontmatterParse:
def test_flat_key_value(self):
text = "name: my-skill\ndescription: Does a thing"
result = _simple_frontmatter_parse(text)
assert result["name"] == "my-skill"
assert result["description"] == "Does a thing"
def test_inline_list(self):
text = "tags: [a, b, c]"
result = _simple_frontmatter_parse(text)
assert result["tags"] == ["a", "b", "c"]
def test_empty_list(self):
text = "tags: []"
result = _simple_frontmatter_parse(text)
assert result["tags"] == []
def test_boolean_true(self):
text = "has_handler: true"
result = _simple_frontmatter_parse(text)
assert result["has_handler"] is True
def test_boolean_false(self):
text = "has_handler: false"
result = _simple_frontmatter_parse(text)
assert result["has_handler"] is False
def test_nested_keys(self):
text = "requires:\n pip: [praw]\n bins: [gh]\n config: [MY_TOKEN]"
result = _simple_frontmatter_parse(text)
assert result["requires"]["pip"] == ["praw"]
assert result["requires"]["bins"] == ["gh"]
assert result["requires"]["config"] == ["MY_TOKEN"]
def test_integer_value(self):
text = "version: 42"
result = _simple_frontmatter_parse(text)
assert result["version"] == 42
def test_quoted_string(self):
text = "description: \"A quoted value\""
result = _simple_frontmatter_parse(text)
assert result["description"] == "A quoted value"
class TestParseSimpleValue:
def test_empty_list(self):
assert _parse_simple_value("[]") == []
def test_inline_list(self):
assert _parse_simple_value("[a, b]") == ["a", "b"]
def test_true(self):
assert _parse_simple_value("true") is True
def test_false(self):
assert _parse_simple_value("false") is False
def test_integer(self):
assert _parse_simple_value("42") == 42
def test_float(self):
assert _parse_simple_value("3.14") == 3.14
def test_string(self):
assert _parse_simple_value("hello") == "hello"
class TestDiscoverSkillsInPath:
def test_finds_catalog_skills(self):
catalog_path = Path(__file__).resolve().parent.parent / "catalog"
skills = discover_skills_in_path(catalog_path, "builtin")
names = {s["name"] for s in skills}
assert "github" in names
assert "system_status" in names
assert "drone_commands" in names
def test_nonexistent_path(self):
skills = discover_skills_in_path("/nonexistent/path", "test")
assert skills == []
def test_empty_dir(self):
with tempfile.TemporaryDirectory() as tmpdir:
skills = discover_skills_in_path(tmpdir, "test")
assert skills == []
def test_skill_dict_structure(self):
catalog_path = Path(__file__).resolve().parent.parent / "catalog"
skills = discover_skills_in_path(catalog_path, "builtin")
for skill in skills:
assert "name" in skill
assert "description" in skill
assert "path" in skill
assert "has_handler" in skill
assert "source" in skill
assert "tags" in skill
def test_has_handler_flag(self):
catalog_path = Path(__file__).resolve().parent.parent / "catalog"
skills = discover_skills_in_path(catalog_path, "builtin")
skill_map = {s["name"]: s for s in skills}
assert skill_map["github"]["has_handler"] is False
assert skill_map["system_status"]["has_handler"] is True
assert skill_map["drone_commands"]["has_handler"] is True
def test_custom_skill_discovery(self):
"""Test that a custom skill directory is discovered correctly."""
with tempfile.TemporaryDirectory() as tmpdir:
skill_dir = Path(tmpdir) / "my-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"---\nname: my-skill\ndescription: A test\n---\n\n# Test\n"
)
skills = discover_skills_in_path(tmpdir, "project")
assert len(skills) == 1
assert skills[0]["name"] == "my-skill"
assert skills[0]["source"] == "project"
class TestParseFrontmatter:
def test_valid_file(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("---\nname: test\ndescription: Hello\n---\n\n# Body\n")
f.flush()
result = parse_frontmatter(f.name)
assert result is not None
assert result["name"] == "test"
Path(f.name).unlink()
def test_invalid_file(self):
result = parse_frontmatter("/nonexistent/file.md")
assert result is None
+180
View File
@@ -0,0 +1,180 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: test_lifecycle.py - Integration test for full skill lifecycle
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/tests
# =============================================
"""Integration tests for the full skill lifecycle: create -> discover -> load -> run."""
import shutil
import sys
import tempfile
from pathlib import Path
import pytest
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from skills.apps.handlers.template import copy_template, get_template
from skills.apps.modules.creator import create_skill
from skills.apps.modules.discovery import discover_skills_in_path, parse_frontmatter
from skills.apps.modules.loader import _load_handler, _parse_full_skill_md
from skills.apps.modules.runner import run_skill
class TestFullLifecycle:
"""Test the complete create -> discover -> load -> run cycle."""
def setup_method(self):
self.tmpdir = tempfile.mkdtemp()
def teardown_method(self):
shutil.rmtree(self.tmpdir)
def test_create_discover_load_markdown_skill(self):
"""Tier 1: Create a markdown skill, discover it, load it, run it."""
# Create
result = create_skill("test-md", template_type="markdown_only", target_dir=self.tmpdir)
assert result["success"] is True
skill_path = Path(result["path"])
assert (skill_path / "SKILL.md").exists()
# Verify placeholder replacement
content = (skill_path / "SKILL.md").read_text()
assert "test-md" in content
assert "{{SKILL_NAME}}" not in content
# Discover
skills = discover_skills_in_path(self.tmpdir, "test")
assert len(skills) == 1
assert skills[0]["name"] == "test-md"
assert skills[0]["has_handler"] is False
# Load (parse full SKILL.md)
metadata, body = _parse_full_skill_md(skill_path / "SKILL.md")
assert metadata is not None
assert metadata["name"] == "test-md"
assert body is not None
def test_create_discover_load_handler_skill(self):
"""Tier 2: Create a handler skill, discover it, load handler."""
# Create
result = create_skill("test-handler", template_type="with_handler", target_dir=self.tmpdir)
assert result["success"] is True
skill_path = Path(result["path"])
assert (skill_path / "SKILL.md").exists()
assert (skill_path / "handler.py").exists()
# Discover
skills = discover_skills_in_path(self.tmpdir, "test")
handler_skill = [s for s in skills if s["name"] == "test-handler"]
assert len(handler_skill) == 1
# Load handler
handler = _load_handler(skill_path, "test-handler")
assert handler is not None
assert hasattr(handler, "run")
assert hasattr(handler, "get_actions")
# Execute handler
actions = handler.get_actions()
assert isinstance(actions, list)
assert len(actions) > 0
# Run an action
result = handler.run(actions[0], args={}, config={})
assert isinstance(result, dict)
assert "success" in result
def test_create_full_structure(self):
"""Tier 3: Create a full 3-layer skill and verify structure."""
result = create_skill("test-full", template_type="full", target_dir=self.tmpdir)
assert result["success"] is True
skill_path = Path(result["path"])
assert (skill_path / "SKILL.md").exists()
assert (skill_path / "apps").is_dir()
assert (skill_path / "apps" / "modules").is_dir()
assert (skill_path / "apps" / "handlers").is_dir()
class TestCatalogSkillsLifecycle:
"""Test that built-in catalog skills work through the full lifecycle."""
def test_github_skill_full_cycle(self):
"""GitHub (Tier 1): discover -> load -> run returns instructions."""
catalog = Path(__file__).resolve().parent.parent / "catalog"
skills = discover_skills_in_path(catalog, "builtin")
github = [s for s in skills if s["name"] == "github"]
assert len(github) == 1
assert github[0]["has_handler"] is False
# Parse full SKILL.md
metadata, body = _parse_full_skill_md(github[0]["path"] / "SKILL.md")
assert metadata["name"] == "github"
assert body is not None
assert "gh" in body.lower()
def test_system_status_full_cycle(self):
"""System status (Tier 2): discover -> load -> run handler."""
result = run_skill("system_status", action="disk")
assert result["success"] is True
assert "Disk Usage" in result["output"]
def test_drone_commands_full_cycle(self):
"""Drone commands (Tier 3): discover -> load -> run handler."""
result = run_skill("drone_commands")
assert result["success"] is True
assert "Available actions" in result["output"]
class TestTemplates:
"""Test template resolution and copying."""
def test_get_markdown_template(self):
result = get_template("markdown_only")
assert result["success"] is True
assert result["path"].exists()
def test_get_handler_template(self):
result = get_template("with_handler")
assert result["success"] is True
assert result["path"].exists()
def test_get_full_template(self):
result = get_template("full")
assert result["success"] is True
assert result["path"].exists()
def test_invalid_template_type(self):
result = get_template("nonexistent")
assert result["success"] is False
assert result["error"] is not None
def test_copy_template_replaces_placeholders(self):
tmpdir = tempfile.mkdtemp()
try:
template = get_template("markdown_only")
target = Path(tmpdir) / "my-skill"
result = copy_template(template["path"], target, "my-skill")
assert result["success"] is True
content = (target / "SKILL.md").read_text()
assert "my-skill" in content
assert "{{SKILL_NAME}}" not in content
finally:
shutil.rmtree(tmpdir)
def test_copy_template_rejects_existing_target(self):
tmpdir = tempfile.mkdtemp()
try:
template = get_template("markdown_only")
target = Path(tmpdir) / "exists"
target.mkdir()
result = copy_template(template["path"], target, "exists")
assert result["success"] is False
assert "already exists" in result["error"]
finally:
shutil.rmtree(tmpdir)
+75
View File
@@ -0,0 +1,75 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: test_loader.py - Unit tests for skills loader
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/tests
# =============================================
"""Tests for the skills loader module."""
import sys
from pathlib import Path
import pytest
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from skills.apps.modules.loader import load_skill
class TestLoadSkill:
def test_load_github_markdown_only(self):
result = load_skill("github")
assert result["success"] is True
assert result["metadata"]["name"] == "github"
assert result["handler"] is None
assert result["body"] is not None
assert len(result["body"]) > 0
def test_load_system_status_with_handler(self):
result = load_skill("system_status")
assert result["success"] is True
assert result["metadata"]["name"] == "system_status"
assert result["handler"] is not None
assert hasattr(result["handler"], "run")
assert hasattr(result["handler"], "get_actions")
def test_load_drone_commands_full(self):
result = load_skill("drone_commands")
assert result["success"] is True
assert result["handler"] is not None
assert hasattr(result["handler"], "run")
def test_load_nonexistent(self):
result = load_skill("nonexistent_skill_xyz")
assert result["success"] is False
assert result["error"] is not None
assert "not found" in result["error"].lower()
assert result["metadata"] is None
assert result["handler"] is None
def test_metadata_has_expected_keys(self):
result = load_skill("github")
metadata = result["metadata"]
assert "name" in metadata
assert "description" in metadata
def test_body_is_markdown_content(self):
result = load_skill("github")
body = result["body"]
assert "# GitHub" in body or "## " in body
def test_handler_contract(self):
"""Verify handler follows the run(action, args, config) contract."""
result = load_skill("system_status")
handler = result["handler"]
# Must have run() and get_actions()
assert callable(handler.run)
assert callable(handler.get_actions)
# get_actions returns a list
actions = handler.get_actions()
assert isinstance(actions, list)
assert len(actions) > 0
+98
View File
@@ -0,0 +1,98 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: test_runner.py - Unit tests for skills runner
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/tests
# =============================================
"""Tests for the skills runner module."""
import sys
from pathlib import Path
import pytest
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from skills.apps.modules.runner import run_skill
class TestRunSkillHandler:
def test_run_system_status_disk(self):
result = run_skill("system_status", action="disk")
assert result["success"] is True
assert "Disk Usage" in result["output"]
assert result["error"] is None
def test_run_system_status_memory(self):
result = run_skill("system_status", action="memory")
assert result["success"] is True
assert "Memory" in result["output"]
def test_run_system_status_uptime(self):
result = run_skill("system_status", action="uptime")
assert result["success"] is True
assert "Uptime" in result["output"]
def test_run_system_status_processes(self):
result = run_skill("system_status", action="processes")
assert result["success"] is True
assert "processes" in result["output"].lower()
def test_run_system_status_summary(self):
result = run_skill("system_status", action="summary")
assert result["success"] is True
assert "Disk Usage" in result["output"]
assert "Memory" in result["output"]
def test_invalid_action(self):
result = run_skill("system_status", action="nonexistent")
assert result["success"] is False
assert result["error"] is not None
def test_no_action_lists_actions(self):
result = run_skill("system_status")
assert result["success"] is True
assert "Available actions" in result["output"]
def test_nonexistent_skill(self):
result = run_skill("nonexistent_skill_xyz")
assert result["success"] is False
assert result["error"] is not None
class TestRunSkillMarkdown:
def test_run_github_returns_body(self):
result = run_skill("github")
assert result["success"] is True
assert result["output"] is not None
assert len(result["output"]) > 100
assert "github" in result["output"].lower()
assert result["error"] is None
def test_output_format(self):
result = run_skill("github")
assert result["output"].startswith("=== Skill: github ===")
class TestRunSkillReturnContract:
def test_return_has_required_keys(self):
result = run_skill("system_status", action="disk")
assert "success" in result
assert "output" in result
assert "error" in result
def test_success_result_types(self):
result = run_skill("system_status", action="disk")
assert isinstance(result["success"], bool)
assert isinstance(result["output"], str)
assert result["error"] is None
def test_failure_result_types(self):
result = run_skill("nonexistent_skill_xyz")
assert isinstance(result["success"], bool)
assert isinstance(result["output"], str)
assert isinstance(result["error"], str)
+89
View File
@@ -0,0 +1,89 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: test_validator.py - Unit tests for skills validator
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/tests
# =============================================
"""Tests for the skills validator handler."""
import sys
from pathlib import Path
import pytest
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from skills.apps.handlers.validator import validate_skill
class TestValidateSkill:
def test_no_requirements(self):
result = validate_skill({})
assert result["valid"] is True
assert result["missing_pip"] == []
assert result["missing_bins"] == []
assert result["missing_config"] == []
def test_empty_requirements(self):
result = validate_skill({"requires": {"pip": [], "bins": [], "config": []}})
assert result["valid"] is True
def test_installed_pip_package(self):
# sys is always available
result = validate_skill({"requires": {"pip": ["sys"]}})
assert result["valid"] is True
assert result["missing_pip"] == []
def test_missing_pip_package(self):
result = validate_skill({"requires": {"pip": ["nonexistent_pkg_xyz_123"]}})
assert result["valid"] is False
assert "nonexistent_pkg_xyz_123" in result["missing_pip"]
def test_available_binary(self):
# python3 should be on PATH
result = validate_skill({"requires": {"bins": ["python3"]}})
assert result["valid"] is True
assert result["missing_bins"] == []
def test_missing_binary(self):
result = validate_skill({"requires": {"bins": ["nonexistent_bin_xyz"]}})
assert result["valid"] is False
assert "nonexistent_bin_xyz" in result["missing_bins"]
def test_missing_config(self):
result = validate_skill({"requires": {"config": ["NONEXISTENT_VAR_XYZ"]}})
assert result["valid"] is False
assert "NONEXISTENT_VAR_XYZ" in result["missing_config"]
def test_set_config(self):
import os
os.environ["_TEST_SKILLS_VAR"] = "value"
try:
result = validate_skill({"requires": {"config": ["_TEST_SKILLS_VAR"]}})
assert result["valid"] is True
assert result["missing_config"] == []
finally:
del os.environ["_TEST_SKILLS_VAR"]
def test_mixed_pass_fail(self):
result = validate_skill({
"requires": {
"pip": ["sys"],
"bins": ["nonexistent_bin_xyz"],
"config": [],
}
})
assert result["valid"] is False
assert result["missing_pip"] == []
assert "nonexistent_bin_xyz" in result["missing_bins"]
def test_return_structure(self):
result = validate_skill({})
assert "valid" in result
assert "missing_pip" in result
assert "missing_bins" in result
assert "missing_config" in result