feat(skills): revive dormant citizen — namespace skills.*→aipass.skills.* (48 imports), path-depth parents[3]→[4], happy-path logger.info() central logging (DPLAN-0203 night shift). 252 tests, seedgo 100%

This commit is contained in:
AIOSAI
2026-06-12 00:25:11 -07:00
parent a797f9d3d3
commit 1871e55b51
90 changed files with 7131 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# .aipass
AIPass local configuration and prompts for the skills branch.
@@ -0,0 +1,58 @@
# SKILLS — Branch Context
<!-- File: src/skills/.aipass/aipass_local_prompt.md — Injected on every prompt when in skills directory. -->
Capability framework for AI agents. Discoverable, validatable, executable skill units across three tiers: markdown-only, with handler, full 3-layer.
## Commands
```
drone @skills list # Show all discovered skills
drone @skills info <name> # Display SKILL.md contents
drone @skills run <name> [action] [args] # Execute a skill's handler
drone @skills create <name> # Scaffold new skill (markdown only)
drone @skills create <name> --with-handler # Scaffold with handler.py
drone @skills create <name> --full # Scaffold with full 3-layer structure
drone @skills validate <name> # Check if skill requirements are met
drone @skills --help # Show help
```
## Apps Layout
```
apps/
├── skills.py # Entry point — command routing
├── modules/
│ ├── discovery.py # Orchestration: discover_all (thin, delegates to handler)
│ ├── loader.py # Orchestration: load_skill (thin, delegates to handler)
│ ├── runner.py # Execute skills (handler-based or markdown-only)
│ ├── creator.py # Scaffold new skills from templates
│ └── validator.py # Check skill requirements
├── handlers/
│ ├── discovery_handler.py # Core: search paths, SKILL.md scanning, frontmatter parsing
│ ├── loader_handler.py # Core: parse full SKILL.md, dynamic handler import
│ ├── registry.py # Build deduplicated skill registry
│ ├── validator.py # Requirement checking (pip, bins, config)
│ └── template.py # Template resolution and copying
├── plugins/ # Extension point (empty)
catalog/ # Built-in skills: drone_commands, github, system_status
templates/ # Skill creation templates (markdown_only, with_handler, full)
```
## Search Paths (first match wins)
1. `.aipass/skills/` — Project-local skills
2. `~/.aipass/skills/` — Global user skills
3. `src/skills/catalog/` — Built-in skills
## Three Skill Tiers
- **Markdown only**: SKILL.md with instructions (AI reads and follows)
- **With handler**: SKILL.md + handler.py (programmatic execution)
- **Full 3-layer**: SKILL.md + apps/ structure (complex skills)
## Memory & Tracking
- `.trinity/passport.json` — identity
- `.trinity/local.json` — session history
- `.trinity/observations.json` — collaboration patterns
- `dev.local.md` — scratchpad for issues, todos, notes
@@ -0,0 +1,27 @@
---
name: another_test
description: TODO — describe what this skill does
version: 1.0.0
tags: []
requires:
pip: []
bins: []
config: []
has_handler: true
---
# another_test
## What This Does
TODO
## When to Use
TODO
## Steps
1. TODO
## Example
```
TODO
```
@@ -0,0 +1,30 @@
"""
another_test skill handler
Called by: drone @skills run another_test <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"]
@@ -0,0 +1,27 @@
---
name: full_test
description: TODO — describe what this skill does
version: 1.0.0
tags: []
requires:
pip: []
bins: []
config: []
has_handler: true
---
# full_test
## What This Does
TODO
## When to Use
TODO
## Steps
1. TODO
## Example
```
TODO
```
@@ -0,0 +1,7 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - full_test apps package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/full_test/apps
# =============================================
@@ -0,0 +1,13 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: __init__.py - full_test handlers package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/full_test/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 - full_test modules package
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/full_test/apps/modules
#
# CHANGELOG (Max 5 entries):
# - v1.0.0 (2026-03-07): Initial scaffold
#
# CODE STANDARDS:
# - Modules layer: orchestration (can print)
# =============================================
@@ -0,0 +1,24 @@
"""
full_test — Full 3-layer skill handler.
Scaffolded by: drone @skills create full_test --full
"""
def run(action: str, args: list, config: dict) -> dict:
"""
Execute the skill.
Args:
action: The action to perform
args: Command arguments
config: Skill configuration from SKILL.md
Returns:
dict with keys: success (bool), output (str), error (str|None)
"""
return {
"success": True,
"output": f"full_test executed action: {action}",
"error": None,
}
@@ -0,0 +1,27 @@
---
name: test_skill
description: TODO — describe what this skill does
version: 1.0.0
tags: []
requires:
pip: []
bins: []
config: []
has_handler: false
---
# test_skill
## What This Does
TODO
## When to Use
TODO
## Steps
1. TODO
## Example
```
TODO
```
+5
View File
@@ -0,0 +1,5 @@
# Claude Code Settings
Claude Code configuration for `Skills`.
Contains `settings.local.json` with permission rules. Most branches are denied raw git commands and must use `drone @git` instead.
+14
View File
@@ -0,0 +1,14 @@
__pycache__/
*.pyc
*.pyo
.env
*.egg-info/
.coverage
htmlcov/
.pytest_cache/
.mypy_cache/
dist/
build/
*.log
*.tmp
*.swp
+3
View File
@@ -0,0 +1,3 @@
# .seedgo
Seedgo audit configuration and bypass rules for the skills branch.
+57
View File
@@ -0,0 +1,57 @@
{
"metadata": {
"version": "1.0.0",
"created": "2026-03-07T23:23:56.244569",
"description": "Standards bypass configuration for this branch"
},
"bypass": [
{
"file": "apps/handlers/loader_handler.py",
"standard": "handlers",
"lines": [25],
"pattern": "from aipass.skills.apps.handlers.discovery_handler import parse_frontmatter",
"reason": "Same-branch handler utility import — parse_frontmatter is a shared parsing function needed by both discovery and loader handlers"
},
{
"file": "apps/handlers/creator_handler.py",
"standard": "handlers",
"lines": [23],
"pattern": "from aipass.skills.apps.handlers.template import copy_template, get_template",
"reason": "Same-branch handler utility import — template.py provides copy/get functions used only by creator_handler"
},
{
"file": "apps/handlers/discovery_handler.py",
"standard": "naming",
"lines": [26],
"pattern": "yaml = None",
"reason": "Conditional import holder — yaml is assigned via 'import yaml' on success or stays None. Not a constant, just a module reference variable"
},
{
"file": "apps/handlers/registry.py",
"standard": "unused_function",
"lines": [52, 68],
"pattern": "def get_skill|def get_skill_names",
"reason": "Public API functions — used by test_registry.py and available for external callers; part of the registry module's contract"
}
],
"notes": {
"usage": "Add entries to 'bypass' list to exclude specific violations",
"example": {
"file": "apps/modules/logger.py",
"standard": "cli",
"lines": [
146,
177
],
"pattern": "if __name__ == '__main__'",
"reason": "Circular dependency - logger cannot import CLI"
},
"fields": {
"file": "Relative path from branch root (required)",
"standard": "Standard name: cli, imports, naming, etc. (required)",
"lines": "Optional - specific line numbers to bypass",
"pattern": "Optional - pattern to match (e.g. 'if __name__')",
"reason": "Required - why this bypass exists"
}
}
}
+169
View File
@@ -0,0 +1,169 @@
[← Back to AIPass](../../../README.md)
# Skills
**Purpose:** Capability framework for AI agents in AIPass. Skills are discoverable, validatable, and executable units of capability that any AI agent can use.
**Module:** `skills`
**Created:** 2026-03-07
**Last Updated:** 2026-04-07
---
## Overview
## 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
## Commands / Usage
```bash
drone @skills list # Show all discovered skills
drone @skills info <name> # Display SKILL.md contents
drone @skills run <name> [action] [args] # Execute a skill's handler
drone @skills create <name> # Scaffold new skill (markdown only)
drone @skills create <name> --with-handler # Scaffold with handler.py
drone @skills create <name> --full # Scaffold with full 3-layer structure
drone @skills validate <name> # Check if skill requirements are met
drone @skills --help # Show help
```
---
## 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
validator.py # Check skill requirements
handlers/
json/ # JSON handler (three-JSON pattern)
creator_handler.py # Skill creation logic (name validation, orchestration)
registry.py # Skill registry management
validator.py # Check requirements
template.py # Skill templates
plugins/ # Plugin extensions
catalog/ # Built-in skills (branch_health, drone_commands, github, inbox_check, system_status)
templates/ # Skill creation templates
skills_json/ # JSON tracking directory
dropbox/ # External storage sync
.trinity/ # Branch identity and memory
tests/ # Test suite
```
---
## Integration Points
### Depends On
- Python stdlib (`pathlib`, `json`, `shutil`, `importlib`, `re`, `yaml`)
- Filesystem: reads SKILL.md files from project, global, and built-in search paths
### Provides To
- All modules — skill discovery, loading, validation, and execution
- AI agents — discoverable capability units via `drone @skills`
- Projects — local skill scaffolding via `drone @skills create`
---
*Last Updated: 2026-04-07*
---
[← Back to AIPass](../../../README.md)
+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
# =============================================
+3
View File
@@ -0,0 +1,3 @@
# apps
Core application code for the skills module.
+15
View File
@@ -0,0 +1,15 @@
# ===================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
# =============================================
from . import handlers # noqa: F401 — required for mock.patch resolution
@@ -0,0 +1,3 @@
# handlers
Low-level handler functions for skill operations.
+124
View File
@@ -0,0 +1,124 @@
"""Skills handlers package - Security protected."""
import inspect
from pathlib import Path
MY_BRANCH = "skills"
MODULE_PATH = "aipass.skills"
def _find_real_caller():
"""
Walk the stack to find the actual file that triggered this import.
Skips:
- This file (handlers/__init__.py)
- Python's importlib internals
- Frozen modules
Returns tuple: (file_path, import_line) or (None, None)
"""
stack = inspect.stack()
this_file = str(Path(__file__).resolve())
for frame_info in stack:
filename = frame_info.filename
# Skip this file
if this_file in str(Path(filename).resolve()):
continue
# Skip Python internals
if filename.startswith("<") or "importlib" in filename:
continue
# Found a real file - try to get the import line
import_line = None
if frame_info.code_context:
import_line = frame_info.code_context[0].strip()
return str(Path(filename).resolve()), import_line
return None, None
def _extract_branch_name(filepath: str) -> str:
"""Extract branch name from a file path."""
parts = Path(filepath).parts
for i, part in enumerate(parts):
if part in ("aipass", "memory", "Nexus"):
if i + 1 < len(parts):
return parts[i + 1]
return "unknown"
def _guard_branch_access():
"""
Block cross-branch handler imports.
Only code from within the 'skills' branch can import these handlers.
External branches must use skills.apps.modules instead.
"""
caller_file, import_line = _find_real_caller()
import os
if os.environ.get("AIPASS_DEBUG_GUARD"):
import sys
print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr)
print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr)
if caller_file is None:
stack = inspect.stack()
for frame in stack:
if frame.filename in ("<string>", "<stdin>"):
target_line = "unknown"
if frame.code_context:
target_line = frame.code_context[0].strip()
raise ImportError(
f"\n{'=' * 60}\n"
f"ACCESS DENIED: Cross-branch handler import blocked\n"
f"{'=' * 60}\n"
f" Caller: interactive/script\n"
f" Blocked: {target_line}\n"
f"\n"
f" Handlers are internal to their branch.\n"
f" Use the module API instead:\n"
f" from {MODULE_PATH}.apps.modules.<module> import <function>\n"
f"\n"
f" For full standards guide:\n"
f" drone @seedgo handlers\n"
f"{'=' * 60}"
)
return
# Check if caller is from our branch
if f"/{MY_BRANCH}/" in caller_file:
return
# External caller - block access
caller_branch = _extract_branch_name(caller_file)
caller_filename = Path(caller_file).name
blocked_import = import_line if import_line else "unknown"
raise ImportError(
f"\n{'=' * 60}\n"
f"ACCESS DENIED: Cross-branch handler import blocked\n"
f"{'=' * 60}\n"
f" Caller branch: {caller_branch}\n"
f" Caller file: {caller_filename}\n"
f" Blocked: {blocked_import}\n"
f"\n"
f" Handlers are internal to their branch.\n"
f" Use the module API instead:\n"
f" from {MODULE_PATH}.apps.modules.<module> import <function>\n"
f"\n"
f" For full standards guide:\n"
f" drone @seedgo handlers\n"
f"{'=' * 60}"
)
# Run guard at import time
_guard_branch_access()
@@ -0,0 +1,110 @@
# =================== AIPass ====================
# Name: creator_handler.py
# Description: Skill creation handler
# Version: 1.2.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""
Skill Creation Handler
Contains the core logic for creating new skills from templates.
Validates skill names, resolves templates, and orchestrates the copy.
Purpose:
Implementation logic for skill creation, separated from CLI/display
layer to satisfy thin-module standard.
"""
from pathlib import Path
from aipass.skills.apps.handlers.json import json_handler
from aipass.skills.apps.handlers.template import copy_template, get_template
# logger imported from aipass.prax
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 in "-_" for c in name) and name == name.lower()
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)
json_handler.log_operation(
"skill_scaffold",
{
"name": name,
"template_type": template_type,
"success": result["success"],
},
)
return {
"success": result["success"],
"path": str(target_path) if result["success"] else None,
"files": result["created_files"],
"error": result["error"],
}
@@ -0,0 +1,268 @@
# =================== AIPass ====================
# Name: discovery_handler.py
# Description: Skill discovery handler
# Version: 1.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""
Skill Discovery Handler
Contains the core logic for discovering skills across search paths.
Scans directories for SKILL.md files and parses YAML frontmatter.
Purpose:
Implementation logic for skill discovery, separated from
orchestration layer to satisfy thin-module standard.
"""
from pathlib import Path
from aipass.prax import logger
from aipass.skills.apps.handlers.json import json_handler
# Try yaml, fall back to simple parser
yaml = None
try:
import yaml
HAS_YAML = True
except ImportError:
logger.warning("yaml package not available — using simple frontmatter parser")
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
if not isinstance(metadata, dict):
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", []),
}
)
json_handler.log_operation(
"discovery_scan",
{
"path": str(path),
"source": source_label,
"found": len(skills),
},
)
return skills
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):
logger.warning(f"Failed to read frontmatter from: {skill_md_path}")
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 yaml is not None:
try:
return yaml.safe_load(frontmatter_text)
except yaml.YAMLError:
logger.warning("YAML parse failed — falling back to simple parser")
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:
logger.warning(f"Could not parse numeric value: {value}")
# String (strip quotes)
return value.strip("'\"")
@@ -0,0 +1 @@
"""Skills JSON handler package."""
@@ -0,0 +1,221 @@
# =================== AIPass ====================
# Name: json_handler.py
# Description: Auto-Creating JSON Handler
# Version: 1.0.0
# Created: 2026-03-17
# Modified: 2026-03-17
# =============================================
"""
JSON Handler - Auto-Creating & Self-Healing JSON System
Handles default JSON files (config, data, log) for skills modules.
Never manually create JSONs - they build themselves.
"""
import json
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional
import inspect
from aipass.prax import logger
# Infrastructure
_BRANCH_ROOT = Path(__file__).resolve().parents[3]
# Constants
SKILLS_JSON_DIR = _BRANCH_ROOT / "skills_json"
def _get_caller_module_name() -> str:
"""
Auto-detect calling module name from call stack.
Returns:
Module name (e.g., "discovery" from discovery.py)
"""
try:
stack = inspect.stack()
# Skip frames: [0]=this function, [1]=log_operation, [2]=actual caller
if len(stack) > 2:
caller_frame = stack[2]
caller_path = Path(caller_frame.filename)
module_name = caller_path.stem
# Validate module name
if module_name and not module_name.startswith("_"):
return module_name
return "unknown"
except Exception:
logger.warning("Failed to detect caller module name from stack")
return "unknown"
def _get_default(json_type: str, module_name: str) -> Any:
"""Return inline default structure for a JSON type."""
now = datetime.now().date().isoformat()
if json_type == "config":
return {
"module_name": module_name,
"version": "1.0.0",
"timestamp": now,
"config": {"auto_save": True, "enabled": True},
}
if json_type == "data":
return {
"module_name": module_name,
"created": now,
"last_updated": now,
"operations_total": 0,
"operations_successful": 0,
"operations_failed": 0,
}
if json_type == "log":
return []
return None
def validate_json_structure(data: Any, json_type: str) -> bool:
"""Validate JSON structure matches expected type."""
if json_type == "config":
if not isinstance(data, dict):
return False
required = ["module_name", "version", "config"]
return all(key in data for key in required)
elif json_type == "data":
if not isinstance(data, dict):
return False
required = ["created", "last_updated"]
return all(key in data for key in required)
elif json_type == "log":
return isinstance(data, list)
return False
def get_json_path(module_name: str, json_type: str) -> Path:
"""Get path for module JSON file."""
filename = f"{module_name}_{json_type}.json"
return SKILLS_JSON_DIR / filename
def ensure_json_exists(module_name: str, json_type: str) -> bool:
"""Ensure JSON file exists, create from template if missing."""
SKILLS_JSON_DIR.mkdir(parents=True, exist_ok=True)
json_path = get_json_path(module_name, json_type)
if json_path.exists():
try:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
if validate_json_structure(data, json_type):
return True
except Exception:
logger.warning(f"Corrupt JSON file, will recreate: {json_path}")
template = _get_default(json_type, module_name)
if template is None:
return False
try:
with open(json_path, "w", encoding="utf-8") as f:
json.dump(template, f, indent=2, ensure_ascii=False)
return True
except Exception:
logger.error(f"Failed to write JSON file: {json_path}")
return False
def load_json(module_name: str, json_type: str) -> Optional[Any]:
"""Load JSON file, auto-create if missing."""
if not ensure_json_exists(module_name, json_type):
return None
json_path = get_json_path(module_name, json_type)
try:
with open(json_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
logger.warning(f"Failed to load JSON: {json_path}")
return None
def save_json(module_name: str, json_type: str, data: Any) -> bool:
"""Save JSON file."""
json_path = get_json_path(module_name, json_type)
if not validate_json_structure(data, json_type):
return False
if json_type == "data" and isinstance(data, dict):
data["last_updated"] = datetime.now().date().isoformat()
try:
with open(json_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
except Exception:
logger.error(f"Failed to save JSON for {module_name}/{json_type}")
return False
def ensure_module_jsons(module_name: str) -> bool:
"""Ensure all 3 JSON files exist for a module."""
ensure_json_exists(module_name, "config")
ensure_json_exists(module_name, "data")
ensure_json_exists(module_name, "log")
return True
def log_operation(operation: str, data: Dict[str, Any] | None = None, module_name: str | None = None) -> bool:
"""
Add entry to module log with automatic rotation.
Auto-detects calling module if module_name not provided.
When max_log_entries is reached, removes oldest entries (FIFO).
Args:
operation: Operation name to log
data: Optional data dict
module_name: Optional module name (auto-detected if not provided)
Returns:
True if successful, False otherwise
"""
if module_name is None:
module_name = _get_caller_module_name()
ensure_module_jsons(module_name)
# Load config to get max_log_entries
config = load_json(module_name, "config")
max_entries = 100
if config and "config" in config:
max_entries = config["config"].get("max_log_entries", 100)
# Load existing log
log = load_json(module_name, "log")
if log is None:
log = []
# Create new entry
entry: Dict[str, Any] = {"timestamp": datetime.now().isoformat(), "operation": operation}
if data:
entry["data"] = data
log.append(entry)
# Rotate if exceeds max
if len(log) > max_entries:
log = log[-max_entries:]
return save_json(module_name, "log", log)
@@ -0,0 +1,185 @@
# =================== AIPass ====================
# Name: loader_handler.py
# Description: Skill loading handler
# Version: 1.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""
Skill Loading Handler
Contains the core logic for loading skills: parsing full SKILL.md files
(frontmatter + body) and dynamically importing handler.py modules.
Purpose:
Implementation logic for skill loading, separated from
orchestration layer to satisfy thin-module standard.
"""
import importlib.util
import sys
from pathlib import Path
from aipass.prax import logger
from aipass.skills.apps.handlers.discovery_handler import parse_frontmatter
from aipass.skills.apps.handlers.json import json_handler
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):
logger.warning(f"Failed to read SKILL.md: {skill_md_path}")
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 import_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:
logger.warning(f"Failed to load handler for {skill_name}")
return None
def find_skill_in_registry(name, registry):
"""Find a skill entry in the registry by name.
Args:
name: Skill name to find.
registry: List of skill dicts.
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 load_skill(name, registry):
"""Load a skill by name from a pre-built registry.
Steps:
1. Find skill in registry
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.
registry: List of skill dicts from discovery.
Returns:
dict: {
"success": bool,
"metadata": dict or None,
"body": str or None,
"handler": module or None,
"path": Path or None,
"error": str or None
}
"""
skill_entry = find_skill_in_registry(name, registry)
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"
# 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}",
}
# Import handler if present
handler = None
if isinstance(metadata, dict) and metadata.get("has_handler", False):
handler = import_handler(skill_path, name)
json_handler.log_operation(
"skill_load",
{
"name": name,
"has_handler": handler is not None,
},
)
return {
"success": True,
"metadata": metadata,
"body": body,
"handler": handler,
"path": skill_path,
"error": None,
}
@@ -0,0 +1,77 @@
# =================== AIPass ====================
# Name: registry.py
# Description: Skill registry management
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
from pathlib import Path
from aipass.skills.apps.handlers.json import json_handler
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)
json_handler.log_operation(
"registry_built",
{
"paths_scanned": len(search_paths),
"skills_found": len(registry),
},
)
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)
@@ -0,0 +1,127 @@
# =================== AIPass ====================
# Name: runner_handler.py
# Description: Skill execution handler
# Version: 1.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""
Skill Execution Handler
Contains the core logic for executing skills: calling handler.run()
for handler-based skills, and assembling output for markdown-only skills.
Purpose:
Implementation logic for skill execution, separated from
orchestration layer to satisfy thin-module standard.
"""
from aipass.prax import logger
from aipass.skills.apps.handlers.json import json_handler
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:
logger.error(f"Failed to list actions for {name}: {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)
json_handler.log_operation(
"handler_executed",
{
"name": name,
"action": action,
"success": True,
},
)
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:
logger.error(f"Skill {name} action '{action}' failed: {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,
}
+121
View File
@@ -0,0 +1,121 @@
# =================== AIPass ====================
# Name: template.py
# Description: Skill template management
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import shutil
from pathlib import Path
from aipass.prax import logger
from aipass.skills.apps.handlers.json import json_handler
# 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 _replace_placeholder_in_file(file_path, skill_name):
"""Replace {{SKILL_NAME}} placeholder in a single file.
Args:
file_path: Path to the file to process.
skill_name: Name to substitute for the placeholder.
"""
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:
logger.warning(f"Skipping binary file during template copy: {file_path}")
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, excluding __pycache__
shutil.copytree(str(template_path), str(target), ignore=shutil.ignore_patterns("__pycache__"))
# Replace placeholders in all files
created_files = []
for file_path in target.rglob("*"):
if not file_path.is_file():
continue
created_files.append(str(file_path.relative_to(target)))
_replace_placeholder_in_file(file_path, skill_name)
json_handler.log_operation(
"template_copied",
{
"template": str(template_path.name),
"target": str(target),
"files_count": len(created_files),
},
)
return {
"success": True,
"created_files": sorted(created_files),
"error": None,
}
except Exception as e:
logger.error(f"Template copy failed: {e}")
# Clean up on failure
if target.exists():
shutil.rmtree(str(target))
return {
"success": False,
"created_files": [],
"error": f"Failed to create skill: {e}",
}
@@ -0,0 +1,114 @@
# =================== AIPass ====================
# Name: validator.py
# Description: Check skill requirements
# Version: 1.0.0
# Created: 2026-03-07
# Modified: 2026-03-07
# =============================================
import importlib.util
import os
import shutil
from aipass.prax import logger
from aipass.skills.apps.handlers.json import json_handler
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)
json_handler.log_operation(
"validation_check",
{
"valid": valid,
"missing_count": len(missing_pip) + len(missing_bins) + len(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):
logger.warning(f"Package check failed for: {pkg}")
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
@@ -0,0 +1,3 @@
# Integrations
Extension point for external integrations.
@@ -0,0 +1,9 @@
{
"module_name": "{{MODULE_NAME}}",
"version": "1.0.0",
"timestamp": "{{TIMESTAMP}}",
"config": {
"auto_save": true,
"enabled": true
}
}
@@ -0,0 +1,8 @@
{
"module_name": "{{MODULE_NAME}}",
"created": "{{TIMESTAMP}}",
"last_updated": "{{TIMESTAMP}}",
"operations_total": 0,
"operations_successful": 0,
"operations_failed": 0
}
@@ -0,0 +1 @@
[]
+3
View File
@@ -0,0 +1,3 @@
# modules
High-level module interfaces that orchestrate handler functions.
@@ -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)
# =============================================
+114
View File
@@ -0,0 +1,114 @@
# =================== AIPass ====================
# Name: creator.py
# Description: Scaffold new skills from templates
# Version: 1.2.0
# Created: 2026-03-07
# Modified: 2026-03-08
# =============================================
"""Skill creator module.
Scaffolds new skills from templates into a target location.
Supports three tiers: markdown_only, with_handler, full.
Thin orchestration layer - delegates to creator_handler for logic.
"""
from aipass.prax import logger
from aipass.cli.apps.modules import console, error
from aipass.skills.apps.handlers.creator_handler import create_skill as _handler_create_skill
from aipass.skills.apps.handlers.json import json_handler
try:
from aipass.trigger.apps.modules.core import trigger
except ImportError:
logger.warning("trigger module not available — skill events disabled")
trigger = None
def handle_command(command: str, args: list) -> bool:
"""Handle commands routed by the entry point.
Args:
command: The subcommand to execute.
args: List of additional arguments.
Returns:
bool: True if command was handled, False otherwise.
"""
if not args:
print_introspection()
return True
if "--help" in args:
print_introspection()
return True
if command == "create":
if not args:
error("Error: skill name required. Usage: skills create <name> [--with-handler|--full]")
return False
name = args[0]
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)
return result["success"]
return False
def create_skill(name, template_type="markdown_only", target_dir=None):
"""Create a new skill from a template.
Delegates to handler for validation and creation logic,
then renders results with Rich.
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}
"""
result = _handler_create_skill(name, template_type=template_type, target_dir=target_dir)
if result["success"]:
if trigger is not None:
trigger.fire("skill_created", name=name, template_type=template_type, path=result["path"])
console.print(f" Created skill '{name}' at {result['path']}")
console.print(f" Template: {template_type}")
console.print(f" Files: {len(result['files'])}")
for f in result["files"]:
console.print(f" - {f}")
json_handler.log_operation(
"skill_created",
{
"name": name,
"template_type": template_type,
"success": result["success"],
},
)
return result
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("[bold cyan]creator Module[/bold cyan]")
console.print("[dim]Scaffold new skills from templates into a target location[/dim]")
console.print()
console.print("[bold]Connected Handlers:[/bold]")
console.print(" [cyan]handlers/[/cyan]")
console.print(
" [dim]- creator_handler.py (create_skill — validate name, resolve template, copy to target)[/dim]"
)
console.print(" [dim]- template.py (copy_template, get_template — template resolution and file copy)[/dim]")
console.print()
+105
View File
@@ -0,0 +1,105 @@
# =================== AIPass ====================
# Name: discovery.py
# Description: Find skills across search paths
# Version: 1.1.0
# Created: 2026-03-07
# Modified: 2026-03-08
# =============================================
"""Skill discovery module.
Thin orchestration layer - delegates to discovery_handler for scanning
search paths and parsing SKILL.md frontmatter.
"""
from aipass.prax import logger # noqa: F401
from aipass.cli.apps.modules import console
from aipass.skills.apps.handlers.discovery_handler import (
get_search_paths,
discover_skills_in_path,
parse_frontmatter, # noqa: F401
)
from aipass.skills.apps.handlers.registry import build_registry
from aipass.skills.apps.handlers.json import json_handler
def handle_command(command: str, args: list) -> bool:
"""Handle commands routed by the entry point.
Args:
command: The subcommand to execute.
args: List of additional arguments.
Returns:
bool: True if command was handled, False otherwise.
"""
if not args:
print_introspection()
return True
if "--help" in args:
print_introspection()
return True
if command in ("discover", "list"):
skills = discover_all()
if not skills:
console.print(" No skills found.")
console.print(" Create one with: drone @skills create <name>")
return True
console.print(f" Found {len(skills)} skill(s):")
console.print()
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)
console.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'])})"
console.print(f" {skill['name']:<25} {skill['description']}{handler_tag}{tags}")
console.print()
return True
return False
def discover_all():
"""Discover all skills across all search paths.
Returns:
list[dict]: All discovered skills, deduplicated by name
(first match wins).
"""
search_paths = get_search_paths()
result = build_registry(search_paths, discover_skills_in_path)
json_handler.log_operation("skills_discovered", {"count": len(result)})
return result
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("[bold cyan]discovery Module[/bold cyan]")
console.print("[dim]Find skills across search paths — project, global, and built-in[/dim]")
console.print()
console.print("[bold]Connected Handlers:[/bold]")
console.print(" [cyan]handlers/[/cyan]")
console.print(
" [dim]- discovery_handler.py (get_search_paths, discover_skills_in_path,"
" parse_frontmatter — path scanning and SKILL.md parsing)[/dim]"
)
console.print(" [dim]- registry.py (build_registry — deduplicated skill registry from search paths)[/dim]")
console.print()
+98
View File
@@ -0,0 +1,98 @@
# =================== AIPass ====================
# Name: loader.py
# Description: Load SKILL.md and handlers
# Version: 1.1.0
# Created: 2026-03-07
# Modified: 2026-03-08
# =============================================
"""Skill loader module.
Thin orchestration layer - delegates to loader_handler for parsing
SKILL.md files and dynamically importing handler modules.
"""
from aipass.cli.apps.modules import console, warning
from aipass.prax import logger
from aipass.skills.apps.modules.discovery import discover_all
from aipass.skills.apps.handlers.loader_handler import load_skill as _handler_load_skill
from aipass.skills.apps.handlers.json import json_handler
def handle_command(command: str, args: list) -> bool:
"""Handle commands routed by the entry point.
Loader is a service module used by other modules (runner, validator, etc.).
It does not handle any direct CLI commands.
Args:
command: The subcommand to execute.
args: List of additional arguments.
Returns:
bool: Always False - loader is a service module, not a command handler.
"""
if not args:
print_introspection()
return True
if "--help" in args:
print_introspection()
return True
return False
def load_skill(name):
"""Load a skill by name.
Discovers all skills, then delegates to handler for loading logic.
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
}
"""
registry = discover_all()
result = _handler_load_skill(name, registry)
if not result["success"]:
logger.warning(f"Failed to load skill: {result['error']}")
if result["success"] and result["handler"] is None and result["metadata"].get("has_handler", False):
warning(f"Warning: has_handler is true but handler.py not found at {result['path']}")
json_handler.log_operation(
"skill_loaded",
{
"name": name,
"success": result["success"],
},
)
return result
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("[bold cyan]loader Module[/bold cyan]")
console.print("[dim]Load SKILL.md metadata, body, and optional handler module by name[/dim]")
console.print()
console.print("[bold]Connected Handlers:[/bold]")
console.print(" [cyan]handlers/[/cyan]")
console.print(
" [dim]- loader_handler.py (load_skill, parse_full_skill_md, import_handler"
" — skill loading and dynamic handler import)[/dim]"
)
console.print()
console.print("[bold]Connected Modules:[/bold]")
console.print(" [cyan]modules/[/cyan]")
console.print(" [dim]- discovery.py (discover_all — skill registry for name lookup)[/dim]")
console.print()
+137
View File
@@ -0,0 +1,137 @@
# =================== AIPass ====================
# Name: runner.py
# Description: Execute skills
# Version: 1.1.0
# Created: 2026-03-07
# Modified: 2026-03-08
# =============================================
"""Skill runner module.
Thin orchestration layer - delegates to runner_handler for executing
skill handlers and assembling markdown output.
"""
from aipass.prax import logger # noqa: F401
from aipass.cli.apps.modules import console, error
from aipass.skills.apps.modules.loader import load_skill
from aipass.skills.apps.handlers.runner_handler import run_handler, run_markdown
from aipass.skills.apps.handlers.json import json_handler
def handle_command(command: str, args: list) -> bool:
"""Handle commands routed by the entry point.
Args:
command: The subcommand to execute.
args: List of additional arguments.
Returns:
bool: True if command was handled, False otherwise.
"""
if not args:
print_introspection()
return True
if "--help" in args:
print_introspection()
return True
if command == "run":
if not args:
error("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_run_args(args[2:]) if len(args) > 2 else {}
result = run_skill(name, action=action, args=extra_args)
if result["success"]:
if result["output"]:
for line in result["output"].splitlines():
console.print(f" {line}")
else:
err = result.get("error", "Unknown error")
error(f"Error: {err}")
return result["success"]
return False
def _parse_run_args(arg_list):
"""Parse extra arguments into a dict."""
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
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"]
# Delegate to handler for execution
if handler is not None:
result = run_handler(handler, name, action, args, config)
else:
result = run_markdown(name, metadata, body)
json_handler.log_operation(
"skill_executed",
{
"name": name,
"success": result["success"],
"has_handler": handler is not None,
},
)
return result
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("[bold cyan]runner Module[/bold cyan]")
console.print("[dim]Execute skills by name — runs handler-based or markdown-only skills[/dim]")
console.print()
console.print("[bold]Connected Handlers:[/bold]")
console.print(" [cyan]handlers/[/cyan]")
console.print(
" [dim]- runner_handler.py (run_handler, run_markdown — skill execution and markdown output)[/dim]"
)
console.print()
console.print("[bold]Connected Modules:[/bold]")
console.print(" [cyan]modules/[/cyan]")
console.print(" [dim]- loader.py (load_skill — load skill metadata, body, and handler)[/dim]")
console.print()
+107
View File
@@ -0,0 +1,107 @@
# =================== AIPass ====================
# Name: validator.py
# Description: Skill validation module
# Version: 1.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
"""Skill validator module.
Thin orchestration layer - delegates to validator handler for
checking skill requirements (pip packages, CLI bins, config/env vars).
"""
from aipass.prax import logger # noqa: F401
from aipass.cli.apps.modules import console, error
from aipass.skills.apps.handlers.validator import validate_skill as _handler_validate
from aipass.skills.apps.handlers.json import json_handler
def handle_command(command: str, args: list) -> bool:
"""Handle commands routed by the entry point.
Args:
command: The subcommand to execute.
args: List of additional arguments.
Returns:
bool: True if command was handled, False otherwise.
"""
if not args:
print_introspection()
return True
if "--help" in args:
print_introspection()
return True
if command == "validate":
if not args:
error("Error: skill name required. Usage: skills validate <name>")
return False
from aipass.skills.apps.modules.loader import load_skill
name = args[0]
loaded = load_skill(name)
if not loaded["success"]:
error(f"Error: {loaded['error']}")
return False
result = validate_skill(loaded["metadata"])
if result["valid"]:
console.print(f" Skill '{name}' - all requirements met.")
else:
console.print(f" Skill '{name}' - requirements NOT met:")
if result["missing_pip"]:
console.print(f" Missing pip packages: {', '.join(result['missing_pip'])}")
if result["missing_bins"]:
console.print(f" Missing CLI tools: {', '.join(result['missing_bins'])}")
if result["missing_config"]:
console.print(f" Missing config/env: {', '.join(result['missing_config'])}")
return result["valid"]
return False
def validate_skill(skill_metadata):
"""Check if a skill's requirements are met.
Delegates to handler for validation logic.
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]
}
"""
result = _handler_validate(skill_metadata)
json_handler.log_operation(
"skill_validated",
{
"valid": result["valid"],
},
)
return result
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("[bold cyan]validator Module[/bold cyan]")
console.print("[dim]Check if a skill's requirements are met (pip packages, CLI bins, config/env vars)[/dim]")
console.print()
console.print("[bold]Connected Handlers:[/bold]")
console.print(" [cyan]handlers/[/cyan]")
console.print(" [dim]- validator.py (validate_skill — check pip, bins, and config requirements)[/dim]")
console.print()
+3
View File
@@ -0,0 +1,3 @@
# plugins
Plugin extensions for the skills system.
@@ -0,0 +1,7 @@
# =================== AIPass ====================
# Name: __init__.py
# Description: Skills plugins package
# Version: 1.0.0
# Created: 2026-03-08
# Modified: 2026-03-08
# =============================================
+330
View File
@@ -0,0 +1,330 @@
# =================== AIPass ====================
# Name: skills.py
# Description: Entry point CLI for drone @skills
# Version: 1.0.1
# Created: 2026-03-08
# Modified: 2026-03-28
# =============================================
import sys
from pathlib import Path
# Prevent this script's parent dir from shadowing the 'skills' package
_script_dir = str(Path(__file__).resolve().parent)
if _script_dir in sys.path:
sys.path.remove(_script_dir)
from aipass.prax import logger # noqa: E402
from aipass.cli.apps.modules import console, error # noqa: E402
"""Skills system entry point.
Provides handle_command(command, args) for drone routing.
Commands: list, info, run, create, validate, --help.
"""
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("[bold cyan]skills Entry Point[/bold cyan]")
console.print("[dim]Capability framework for AI agents — discover, run, create, and validate skills[/dim]")
console.print()
console.print("[bold]Connected Modules:[/bold]")
console.print(" [cyan]modules/[/cyan]")
console.print(" [dim]- discovery.py (discover_all — scan search paths for skills)[/dim]")
console.print(" [dim]- loader.py (load_skill — load SKILL.md metadata, body, and handler)[/dim]")
console.print(" [dim]- runner.py (run_skill — execute handler-based or markdown-only skills)[/dim]")
console.print(" [dim]- creator.py (create_skill — scaffold new skills from templates)[/dim]")
console.print(" [dim]- validator.py (validate_skill — check skill requirements)[/dim]")
console.print()
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 is None:
print_introspection()
return True
if command in ("--help", "-h", "help"):
print_help()
return True
if command in ("--version", "-V"):
console.print("SKILLS v1.0.0")
return True
if command == "list":
return _cmd_list()
if command == "info":
if not args:
error("Error: skill name required. Usage: skills info <name>")
return False
return _cmd_info(args[0])
if command == "run":
if not args:
error("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:
error("Error: skill name required. Usage: skills create <name> [--with-handler|--full]")
return False
if args[0] in ("--help", "-h", "help"):
_print_create_help()
return True
return _cmd_create(args)
if command == "validate":
if not args:
error("Error: skill name required. Usage: skills validate <name>")
return False
return _cmd_validate(args[0])
console.print(f" Unknown command: {command}")
console.print(" Run 'skills --help' for available commands.")
return False
def print_help():
"""Print skills help text."""
console.print("Skills - Capability framework for AI agents")
console.print()
console.print("Usage:")
console.print(" drone @skills <command> [args]")
console.print()
console.print("Commands:")
console.print(" list Show all discovered skills")
console.print(" info <name> Display SKILL.md contents")
console.print(" run <name> [action] [args] Execute a skill's handler")
console.print(" create <name> Scaffold new skill (markdown only)")
console.print(" create <name> --with-handler Scaffold with handler.py")
console.print(" create <name> --full Scaffold with full 3-layer structure")
console.print(" validate <name> Check if skill requirements are met")
console.print(" --help Show this help")
console.print(" --version, -V Show version")
console.print()
console.print("Search paths (first match wins):")
console.print(" 1. .aipass/skills/ Project-local skills")
console.print(" 2. ~/.aipass/skills/ Global user skills")
console.print(" 3. src/skills/catalog/ Built-in skills")
def _cmd_list():
"""List all discovered skills."""
from aipass.skills.apps.modules.discovery import discover_all
skills = discover_all()
if not skills:
console.print(" No skills found.")
console.print(" Create one with: drone @skills create <name>")
return True
logger.info(f"list: found {len(skills)} skill(s)")
console.print(f" Found {len(skills)} skill(s):")
console.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)
console.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'])})"
console.print(f" {skill['name']:<25} {skill['description']}{handler_tag}{tags}")
console.print()
return True
def _cmd_info(name):
"""Display full SKILL.md contents for a skill."""
from aipass.skills.apps.modules.loader import load_skill
loaded = load_skill(name)
if not loaded["success"]:
error(f"Error: {loaded['error']}")
return False
metadata = loaded["metadata"]
body = loaded["body"]
path = loaded["path"]
console.print(f" Skill: {metadata.get('name', name)}")
console.print(f" Version: {metadata.get('version', 'unknown')}")
console.print(f" Description: {metadata.get('description', 'No description')}")
console.print(f" Path: {path}")
console.print(f" Has Handler: {metadata.get('has_handler', False)}")
tags = metadata.get("tags", [])
if tags:
console.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:
console.print(f" Requires pip: {', '.join(pip_pkgs)}")
if bins:
console.print(f" Requires bins: {', '.join(bins)}")
if config:
console.print(f" Requires config: {', '.join(config)}")
if body:
console.print()
console.print(" --- SKILL.md Body ---")
for line in body.splitlines():
console.print(f" {line}")
logger.info(f"info: loaded skill '{name}'")
return True
def _cmd_run(name, action, extra_args):
"""Execute a skill."""
from aipass.skills.apps.modules.runner import run_skill
result = run_skill(name, action=action, args=extra_args)
if result["success"]:
logger.info(f"run: executed skill '{name}' action={action}")
if result["output"]:
for line in result["output"].splitlines():
console.print(f" {line}")
else:
err = result.get("error", "Unknown error")
error(f"Error: {err}")
return result["success"]
def _print_create_help():
"""Print help text for the create subcommand."""
console.print("Skills Create - Scaffold a new skill from a template")
console.print()
console.print("Usage:")
console.print(" drone @skills create <name> Create a markdown-only skill")
console.print(" drone @skills create <name> --with-handler Create with handler.py")
console.print(" drone @skills create <name> --full Create with full 3-layer structure")
console.print()
console.print("Templates:")
console.print(" markdown_only SKILL.md with instructions (AI reads and follows)")
console.print(" with_handler SKILL.md + handler.py (programmatic execution)")
console.print(" full SKILL.md + apps/ structure (complex skills)")
def _cmd_create(args):
"""Create a new skill from a template."""
from aipass.skills.apps.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"]:
error(f"Error: {result['error']}")
return False
logger.info(f"create: scaffolded skill '{name}' ({template_type})")
return True
def _cmd_validate(name):
"""Validate a skill's requirements."""
from aipass.skills.apps.modules.loader import load_skill
from aipass.skills.apps.modules.validator import validate_skill
loaded = load_skill(name)
if not loaded["success"]:
error(f"Error: {loaded['error']}")
return False
result = validate_skill(loaded["metadata"])
if result["valid"]:
logger.info(f"validate: skill '{name}' passed all requirements")
console.print(f" Skill '{name}' - all requirements met.")
else:
console.print(f" Skill '{name}' - requirements NOT met:")
if result["missing_pip"]:
console.print(f" Missing pip packages: {', '.join(result['missing_pip'])}")
if result["missing_bins"]:
console.print(f" Missing CLI tools: {', '.join(result['missing_bins'])}")
if result["missing_config"]:
console.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
if __name__ == "__main__":
import sys
args = sys.argv[1:]
if not args:
handle_command("--help")
else:
command = args[0]
remaining = args[1:] if len(args) > 1 else []
handle_command(command, remaining)
View File
@@ -0,0 +1,46 @@
---
name: branch_health
description: Quick health check -- test counts and file stats for AIPass branches
version: 1.0.0
tags: [system, monitoring, health, testing]
requires:
pip: []
bins: []
config: []
has_handler: true
---
# Branch Health Skill
Quick health check for AIPass branches. Counts Python source files, test files, and test functions to give a snapshot of each branch's codebase and test coverage.
## Available Actions
| Action | Description |
|-------------|-------------------------------------------------------|
| `summary` | Full stats for all branches (default) |
| `tests` | Test-only stats (test files, test function counts) |
| *branch* | Stats for a single branch by name |
## Usage
```bash
drone @skills run branch_health summary
drone @skills run branch_health tests
drone @skills run branch_health flow
```
## Output Format
All actions return structured dicts:
```python
{"success": True, "output": "...", "error": None}
```
## Notes
- Scans `apps/` for source files and `tests/` for test files
- Counts `def test_` lines as test functions
- Missing directories are handled gracefully
- No external dependencies -- stdlib only
@@ -0,0 +1,226 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: handler.py - Branch Health skill handler
# Date: 2026-03-29
# Version: 1.0.0
# Category: skills/catalog/branch_health
# =============================================
"""
Branch Health skill handler.
Quick health check for AIPass branches -- counts Python source files,
test files, and test functions per branch.
Called by: drone @skills run branch_health <action>
"""
from pathlib import Path
def run(action, args=None, config=None):
"""Execute a branch health action.
Args:
action: One of: summary (default), tests, or a specific branch name
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 {}
try:
if action == "summary":
return _full_summary()
if action == "tests":
return _tests_only()
return _single_branch(action)
except Exception as exc:
return {
"success": False,
"output": "",
"error": f"Action '{action}' failed: {exc}",
}
def get_actions():
"""List available actions for this skill."""
return ["summary", "tests", "<branch_name>"]
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _src_root():
"""Return the src/ directory by navigating up from this handler."""
# handler.py -> branch_health/ -> catalog/ -> skills/ -> aipass/ -> src/
return Path(__file__).resolve().parents[4]
def _find_branches():
"""Yield (branch_name, branch_path) for all branches."""
src = _src_root()
# src/aipass/*/ branches
aipass_dir = src / "aipass"
if aipass_dir.is_dir():
for branch_dir in sorted(aipass_dir.iterdir()):
if branch_dir.is_dir() and not branch_dir.name.startswith((".", "_")):
# Only yield actual branches (have apps/ or tests/ or .trinity/)
if (
(branch_dir / "apps").is_dir()
or (branch_dir / "tests").is_dir()
or (branch_dir / ".trinity").is_dir()
):
yield (branch_dir.name, branch_dir)
# src/skills/ itself
skills_dir = src / "skills"
if skills_dir.is_dir():
yield ("skills", skills_dir)
def _count_py_files(directory):
"""Count .py files recursively in a directory."""
if not directory.is_dir():
return 0
return sum(1 for _ in directory.rglob("*.py"))
def _count_test_files(directory):
"""Count test_*.py files in a directory."""
if not directory.is_dir():
return 0
return sum(1 for f in directory.rglob("*.py") if f.name.startswith("test_"))
def _count_test_functions(directory):
"""Count lines matching 'def test_' in test files."""
if not directory.is_dir():
return 0
count = 0
for py_file in directory.rglob("*.py"):
if not py_file.name.startswith("test_"):
continue
try:
text = py_file.read_text(encoding="utf-8")
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("def test_"):
count += 1
except OSError:
continue
return count
def _branch_stats(branch_name, branch_path):
"""Compute stats for a single branch. Returns a dict."""
apps_dir = branch_path / "apps"
tests_dir = branch_path / "tests"
return {
"name": branch_name,
"py_files": _count_py_files(apps_dir),
"test_files": _count_test_files(tests_dir),
"test_functions": _count_test_functions(tests_dir),
"has_apps": apps_dir.is_dir(),
"has_tests": tests_dir.is_dir(),
}
def _format_row(name, py_files, test_files, test_fns):
"""Format a single branch stats row."""
return f" {name:<20s} {py_files:>5d} py {test_files:>4d} tests {test_fns:>5d} fns"
def _full_summary():
"""Full stats for all branches."""
lines = ["Branch Health Summary", " " + "-" * 55]
total_py = 0
total_tests = 0
total_fns = 0
branch_count = 0
for branch_name, branch_path in _find_branches():
stats = _branch_stats(branch_name, branch_path)
lines.append(
_format_row(
stats["name"],
stats["py_files"],
stats["test_files"],
stats["test_functions"],
)
)
total_py += stats["py_files"]
total_tests += stats["test_files"]
total_fns += stats["test_functions"]
branch_count += 1
lines.append(" " + "-" * 55)
lines.append(f" {'TOTAL':<20s} {total_py:>5d} py {total_tests:>4d} tests {total_fns:>5d} fns")
lines.append(f" ({branch_count} branches)")
return {"success": True, "output": "\n".join(lines), "error": None}
def _tests_only():
"""Test-only stats for all branches."""
lines = ["Branch Health -- Test Stats", " " + "-" * 45]
total_tests = 0
total_fns = 0
for branch_name, branch_path in _find_branches():
stats = _branch_stats(branch_name, branch_path)
if stats["test_files"] > 0 or stats["test_functions"] > 0:
lines.append(f" {stats['name']:<20s} {stats['test_files']:>4d} tests {stats['test_functions']:>5d} fns")
total_tests += stats["test_files"]
total_fns += stats["test_functions"]
if total_tests == 0:
lines.append(" No test files found.")
else:
lines.append(" " + "-" * 45)
lines.append(f" {'TOTAL':<20s} {total_tests:>4d} tests {total_fns:>5d} fns")
return {"success": True, "output": "\n".join(lines), "error": None}
def _single_branch(branch_name):
"""Stats for a single branch."""
src = _src_root()
# Check src/aipass/<branch_name>/ first, then src/<branch_name>/
candidates = [
src / "aipass" / branch_name,
src / branch_name,
]
branch_path = None
for candidate in candidates:
if candidate.is_dir():
branch_path = candidate
break
if branch_path is None:
return {
"success": True,
"output": f"Branch Health -- {branch_name}\n Branch '{branch_name}' not found.",
"error": None,
}
stats = _branch_stats(branch_name, branch_path)
lines = [
f"Branch Health -- {branch_name}",
f" Source files (apps/): {stats['py_files']}",
f" Test files (tests/): {stats['test_files']}",
f" Test functions: {stats['test_functions']}",
f" Has apps/ dir: {'yes' if stats['has_apps'] else 'no'}",
f" Has tests/ dir: {'yes' if stats['has_tests'] else 'no'}",
]
return {"success": True, "output": "\n".join(lines), "error": None}
@@ -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
@@ -0,0 +1,46 @@
---
name: inbox_check
description: Check ai_mail inbox status across AIPass branches
version: 1.0.0
tags: [communication, mail, status]
requires:
pip: []
bins: []
config: []
has_handler: true
---
# Inbox Check Skill
Scan AIPass branches for `.ai_mail.local/inbox.json` files and report unread message counts. Useful for quickly seeing which branches have pending mail without visiting each one.
## Available Actions
| Action | Description |
|-------------|-----------------------------------------------------|
| `summary` | Unread counts per branch (default) |
| `all` | Full message listing for every branch |
| *branch* | Show inbox for a specific branch by name |
## Usage
```bash
drone @skills run inbox_check summary
drone @skills run inbox_check all
drone @skills run inbox_check flow
```
## Output Format
All actions return structured dicts:
```python
{"success": True, "output": "...", "error": None}
```
## Notes
- Reads `.ai_mail.local/inbox.json` from each branch directory
- Messages with `"status": "new"` are counted as unread
- Missing inbox files are silently skipped in summary mode
- No external dependencies -- stdlib only
@@ -0,0 +1,185 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: handler.py - Inbox Check skill handler
# Date: 2026-03-29
# Version: 1.0.0
# Category: skills/catalog/inbox_check
# =============================================
"""
Inbox Check skill handler.
Scan AIPass branches for .ai_mail.local/inbox.json and report
unread message counts or full message listings.
Called by: drone @skills run inbox_check <action>
"""
import json
from pathlib import Path
def run(action, args=None, config=None):
"""Execute an inbox check action.
Args:
action: One of: summary (default), all, or a specific branch name
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 {}
try:
if action in ("summary", "all"):
return _scan_all(detail=(action == "all"))
return _scan_branch(action)
except Exception as exc:
return {
"success": False,
"output": "",
"error": f"Action '{action}' failed: {exc}",
}
def get_actions():
"""List available actions for this skill."""
return ["summary", "all", "<branch_name>"]
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _src_root():
"""Return the src/ directory by navigating up from this handler."""
# handler.py -> inbox_check/ -> catalog/ -> skills/ -> aipass/ -> src/
return Path(__file__).resolve().parents[4]
def _find_inboxes():
"""Yield (branch_name, inbox_path) for all branches with inbox files."""
src = _src_root()
# src/aipass/*/ branches
aipass_dir = src / "aipass"
if aipass_dir.is_dir():
for branch_dir in sorted(aipass_dir.iterdir()):
if branch_dir.is_dir():
inbox = branch_dir / ".ai_mail.local" / "inbox.json"
if inbox.is_file():
yield (branch_dir.name, inbox)
# src/skills/ itself
skills_inbox = src / "skills" / ".ai_mail.local" / "inbox.json"
if skills_inbox.is_file():
yield ("skills", skills_inbox)
def _read_inbox(inbox_path):
"""Read and parse an inbox.json file. Returns list of messages."""
try:
text = inbox_path.read_text(encoding="utf-8").strip()
if not text:
return []
data = json.loads(text)
if isinstance(data, list):
return data
if isinstance(data, dict) and "messages" in data:
return data["messages"]
return []
except (json.JSONDecodeError, OSError):
return []
def _count_new(messages):
"""Count messages where status == 'new'."""
return sum(1 for m in messages if isinstance(m, dict) and m.get("status") == "new")
def _scan_all(detail=False):
"""Scan all branches for inbox status."""
lines = []
total_new = 0
total_messages = 0
branch_count = 0
for branch_name, inbox_path in _find_inboxes():
messages = _read_inbox(inbox_path)
new_count = _count_new(messages)
total_new += new_count
total_messages += len(messages)
branch_count += 1
if detail:
lines.append(f"\n {branch_name} ({new_count} new / {len(messages)} total):")
if messages:
for msg in messages:
if not isinstance(msg, dict):
continue
status = msg.get("status", "unknown")
sender = msg.get("from", msg.get("sender", "unknown"))
subject = msg.get("subject", msg.get("message", "(no subject)"))
marker = "*" if status == "new" else " "
lines.append(f" {marker} [{status}] from {sender}: {subject}")
else:
lines.append(" (empty)")
else:
if new_count > 0:
lines.append(f" {branch_name}: {new_count} new ({len(messages)} total)")
if not lines and not detail:
output = "Inbox Check\n No unread messages across any branch."
else:
header = f"Inbox Check -- {branch_count} branches scanned"
summary = f" Total: {total_new} new / {total_messages} messages"
body = "\n".join(lines) if lines else " No unread messages."
output = f"{header}\n{summary}\n{body}"
return {"success": True, "output": output, "error": None}
def _scan_branch(branch_name):
"""Show inbox for a specific branch."""
src = _src_root()
# Check src/aipass/<branch_name>/ first, then src/<branch_name>/
candidates = [
src / "aipass" / branch_name / ".ai_mail.local" / "inbox.json",
src / branch_name / ".ai_mail.local" / "inbox.json",
]
inbox_path = None
for candidate in candidates:
if candidate.is_file():
inbox_path = candidate
break
if inbox_path is None:
return {
"success": True,
"output": f"Inbox Check -- {branch_name}\n No inbox found for branch '{branch_name}'.",
"error": None,
}
messages = _read_inbox(inbox_path)
new_count = _count_new(messages)
lines = [f"Inbox Check -- {branch_name} ({new_count} new / {len(messages)} total):"]
if messages:
for msg in messages:
if not isinstance(msg, dict):
continue
status = msg.get("status", "unknown")
sender = msg.get("from", msg.get("sender", "unknown"))
subject = msg.get("subject", msg.get("message", "(no subject)"))
marker = "*" if status == "new" else " "
lines.append(f" {marker} [{status}] from {sender}: {subject}")
else:
lines.append(" (empty inbox)")
return {"success": True, "output": "\n".join(lines), "error": None}
@@ -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)
@@ -0,0 +1,243 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: handler.py - System Status skill handler
# Date: 2026-03-07
# Version: 1.0.0
# Category: skills/catalog/system_status
# =============================================
"""
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 Total: {total}\n Used: {used} ({percent:.1f}%)\n 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}
View File
+3
View File
@@ -0,0 +1,3 @@
# docs
Public documentation for the skills module.
+22
View File
@@ -0,0 +1,22 @@
[pytest]
# Test discovery paths
testpaths = tests
# Test file patterns
python_files = test_*.py
python_functions = test_*
python_classes = Test*
# Command-line options (always applied)
# Verbose output, short traceback, strict markers, show summary of all outcomes
addopts =
-v
--tb=short
--strict-markers
-ra
# Test markers (for categorizing tests)
markers =
unit: Unit tests
integration: Integration tests
slow: Tests that take significant time
+3
View File
@@ -0,0 +1,3 @@
# templates
Skill scaffolding templates (markdown_only, with_handler, full).
+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,7 @@
# ===================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
# =============================================
@@ -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,24 @@
"""
{{SKILL_NAME}} — Full 3-layer skill handler.
Scaffolded by: drone @skills create {{SKILL_NAME}} --full
"""
def run(action: str, args: list, config: dict) -> dict:
"""
Execute the skill.
Args:
action: The action to perform
args: Command arguments
config: Skill configuration from SKILL.md
Returns:
dict with keys: success (bool), output (str), error (str|None)
"""
return {
"success": True,
"output": f"{{SKILL_NAME}} executed action: {action}",
"error": None,
}
@@ -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"]
+3
View File
@@ -0,0 +1,3 @@
# tests
Test suite for the skills module.
+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
# =============================================
+178
View File
@@ -0,0 +1,178 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: conftest.py - Skills test configuration
# Date: 2026-03-07
# Version: 2.0.0
# Category: skills/tests
#
# CHANGELOG (Max 5 entries):
# - v2.0.0 (2026-03-28): Added temp_dir, sample_data, mock_infrastructure,
# mock_logger, mock_json_handler fixtures for test quality compliance
# - v1.0.0 (2026-03-07): Initial implementation
#
# CODE STANDARDS:
# - Adds skills root to sys.path for test imports
# =============================================
"""Skills test configuration."""
import os
import tempfile
# Redirect prax logs to temp directory during tests
# Must be set before any prax imports to catch logger initialization
if "AIPASS_TEST_LOG_DIR" not in os.environ:
os.environ["AIPASS_TEST_LOG_DIR"] = tempfile.mkdtemp(prefix="aipass_test_logs_")
import importlib
import logging
import sys
import types
from pathlib import Path
from typing import Generator
from unittest.mock import MagicMock
import pytest
# Add src/ to path so aipass.skills is importable
skills_root = Path(__file__).resolve().parents[3]
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
# ---------------------------------------------------------------------------
# Dynamic import for json_handler isolation
# ---------------------------------------------------------------------------
BRANCH_MODULE = "aipass.skills"
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
# Ensure the handler package is importable
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
_handlers_dir = Path(__file__).resolve().parents[1] / "apps" / "handlers"
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_json_mod = importlib.import_module(_json_mod_path)
# ---------------------------------------------------------------------------
# JSON_DIR variable discovery
# ---------------------------------------------------------------------------
_JSON_DIR_ATTR: str | None = None
_JSON_DIR_CANDIDATES = [
"SKILLS_JSON_DIR",
"JSON_DIR",
"BRANCH_JSON_DIR",
"_JSON_DIR",
]
for _candidate in _JSON_DIR_CANDIDATES:
if hasattr(_json_mod, _candidate):
_JSON_DIR_ATTR = _candidate
break
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture()
def temp_dir(tmp_path: Path) -> Generator[Path, None, None]:
"""Creates temporary directory for testing, cleans up after."""
test_dir = tmp_path / "test_workspace"
test_dir.mkdir(parents=True, exist_ok=True)
yield test_dir
for child in test_dir.iterdir():
if child.is_file():
child.unlink()
@pytest.fixture()
def sample_data() -> dict:
"""Sample test data for JSON operations."""
return {
"config": {
"module_name": "test_module",
"version": "1.0.0",
"config": {"max_log_entries": 50},
"timestamp": "2026-03-28",
},
"data": {
"module_name": "test_module",
"created": "2026-03-28",
"last_updated": "2026-03-28",
"operations_total": 0,
"operations_successful": 0,
"operations_failed": 0,
},
"log": [{"timestamp": "2026-03-28T10:00:00", "operation": "test"}],
}
@pytest.fixture(autouse=True)
def mock_infrastructure(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Autouse fixture that isolates JSON operations and silences logging.
This fixture:
1. Redirects the branch's JSON_DIR to tmp_path (test isolation)
2. Patches the branch logger to a NullHandler (no console noise)
"""
if _JSON_DIR_ATTR is not None:
monkeypatch.setattr(_json_mod, _JSON_DIR_ATTR, tmp_path)
logger_names = [
BRANCH_MODULE,
f"{BRANCH_MODULE}.apps.handlers.json.json_handler",
]
for logger_name in logger_names:
log = logging.getLogger(logger_name)
monkeypatch.setattr(log, "handlers", [logging.NullHandler()])
@pytest.fixture()
def mock_logger() -> MagicMock:
"""Standalone mock logger for tests that need to verify logging calls."""
mock = MagicMock(spec=logging.Logger)
mock.debug = MagicMock()
mock.info = MagicMock()
mock.warning = MagicMock()
mock.error = MagicMock()
mock.critical = MagicMock()
return mock
@pytest.fixture()
def mock_json_handler() -> MagicMock:
"""Standalone mock json_handler for isolating from real file I/O."""
handler = MagicMock()
handler.load_json = MagicMock(return_value={})
handler.save_json = MagicMock(return_value=True)
handler.ensure_json_exists = MagicMock(return_value=True)
handler.ensure_module_jsons = MagicMock(return_value=True)
handler.get_json_path = MagicMock(return_value=Path("/tmp/mock.json"))
handler.validate_json_structure = MagicMock(return_value=True)
handler.log_operation = MagicMock(return_value=True)
return handler
@pytest.fixture()
def reimport_after_mock(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
"""Fixture demonstrating reimport_after_mock pattern.
Patches sys.modules to inject a mock, then reimports the handler module
so it picks up the mocked dependency. Useful for testing import-time behavior.
"""
mock_mod = MagicMock()
monkeypatch.setitem(sys.modules, f"{BRANCH_MODULE}.apps.handlers.json.json_handler", mock_mod)
reimported = importlib.import_module(_json_mod_path)
importlib.reload(reimported)
return mock_mod
+205
View File
@@ -0,0 +1,205 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: test_cli_routing.py - Unit tests for skills.py CLI routing
# Date: 2026-03-10
# Version: 1.0.0
# Category: skills/tests
# =============================================
"""Tests for the skills entry point CLI routing."""
import sys
from pathlib import Path
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from aipass.skills.apps.skills import handle_command, _parse_extra_args
class TestParseExtraArgs:
def test_key_value_pairs(self):
result = _parse_extra_args(["host=localhost", "port=8080"])
assert result == {"host": "localhost", "port": "8080"}
def test_positional_args(self):
result = _parse_extra_args(["foo", "bar"])
assert result == {"arg0": "foo", "arg1": "bar"}
def test_mixed_args(self):
result = _parse_extra_args(["foo", "key=val", "bar"])
assert result == {"arg0": "foo", "key": "val", "arg1": "bar"}
def test_empty_args(self):
result = _parse_extra_args([])
assert result == {}
def test_value_with_equals_sign(self):
"""key=value where value itself contains '='."""
result = _parse_extra_args(["query=a=b"])
assert result == {"query": "a=b"}
class TestHandleCommand:
def test_none_command_shows_introspection(self):
result = handle_command(None)
assert result is True
def test_help_command(self):
result = handle_command("--help")
assert result is True
def test_help_alias(self):
result = handle_command("help")
assert result is True
def test_h_flag(self):
result = handle_command("-h")
assert result is True
def test_version_command(self):
result = handle_command("--version")
assert result is True
def test_version_short_flag(self):
result = handle_command("-V")
assert result is True
def test_unknown_command_returns_false(self):
result = handle_command("bogus_command_xyz")
assert result is False
def test_list_command(self):
result = handle_command("list")
assert result is True
def test_info_missing_args_returns_false(self):
result = handle_command("info")
assert result is False
def test_info_with_valid_skill(self):
result = handle_command("info", ["github"])
assert result is True
def test_run_missing_args_returns_false(self):
result = handle_command("run")
assert result is False
def test_run_with_valid_skill(self):
result = handle_command("run", ["system_status", "disk"])
assert result is True
def test_validate_missing_args_returns_false(self):
result = handle_command("validate")
assert result is False
def test_validate_with_valid_skill(self):
result = handle_command("validate", ["github"])
assert result is True
def test_create_missing_args_returns_false(self):
result = handle_command("create")
assert result is False
def test_create_help_flag_returns_true(self):
"""create --help shows help instead of treating --help as a skill name."""
result = handle_command("create", ["--help"])
assert result is True
def test_create_help_flag_shows_usage(self, capsys):
"""create --help prints usage text."""
handle_command("create", ["--help"])
captured = capsys.readouterr()
assert "Usage" in captured.out
assert "create" in captured.out.lower()
def test_create_h_flag_returns_true(self):
"""create -h shows help."""
result = handle_command("create", ["-h"])
assert result is True
def test_create_help_word_returns_true(self):
"""create help shows help."""
result = handle_command("create", ["help"])
assert result is True
# ===================================================================
# Missing coverage: no_args, print_help, print_introspection, output_capture
# ===================================================================
class TestNoArgs:
"""Test no_args behavior -- None command triggers introspection."""
def test_no_args_returns_true(self):
"""no_args: handle_command(None) returns True."""
result = handle_command(None)
assert result is True
def test_no_args_triggers_introspection(self, capsys):
"""no_args_triggers: calling with None produces introspection output."""
handle_command(None)
captured = capsys.readouterr()
assert "skills" in captured.out.lower() or "Entry Point" in captured.out
class TestPrintHelp:
"""Tests for print_help output."""
def test_print_help_produces_output(self, capsys):
"""print_help: calling --help produces help text."""
from aipass.skills.apps.skills import print_help
print_help()
captured = capsys.readouterr()
assert "Usage" in captured.out or "Commands" in captured.out
def test_print_help_via_command(self, capsys):
"""print_help: handle_command('--help') produces output."""
handle_command("--help")
captured = capsys.readouterr()
assert len(captured.out) > 0
class TestPrintIntrospection:
"""Tests for print_introspection output."""
def test_print_introspection_produces_output(self, capsys):
"""print_introspection: shows module info."""
from aipass.skills.apps.skills import print_introspection
print_introspection()
captured = capsys.readouterr()
assert "Entry Point" in captured.out or "skills" in captured.out.lower()
def test_print_introspection_lists_modules(self, capsys):
"""print_introspection: lists connected modules."""
from aipass.skills.apps.skills import print_introspection
print_introspection()
captured = capsys.readouterr()
assert "modules/" in captured.out or "discovery" in captured.out.lower()
class TestOutputCapture:
"""Tests using capsys for output_capture verification."""
def test_output_capture_help_command(self, capsys):
"""output_capture: --help produces non-empty stdout."""
handle_command("--help")
captured = capsys.readouterr()
assert captured.out != ""
def test_output_capture_version_command(self, capsys):
"""output_capture: --version produces version string."""
handle_command("--version")
captured = capsys.readouterr()
assert "SKILLS" in captured.out or "1.0.0" in captured.out
def test_output_capture_unknown_command(self, capsys):
"""output_capture: unknown command produces output."""
handle_command("bogus_xyz")
captured = capsys.readouterr()
assert "Unknown command" in captured.out or "unknown" in captured.out.lower() or len(captured.out) > 0
+132
View File
@@ -0,0 +1,132 @@
# =================== AIPass ====================
# Name: test_contracts.py
# Description: Contract Tests (return types, exceptions, data structures)
# Version: 1.0.0
# Created: 2026-03-28
# Modified: 2026-03-28
# =============================================
"""
Contract Tests for skills branch.
Covers 3 groups:
- Return type contracts (4): command_returns_bool, paths_return_path,
ensure_returns_bool, load_correct_type
- Exception contracts (3): create_default_raises, save_invalid_raises,
invalid_mode_raises
- Data structure contracts (3): config_keys, data_keys, log_entry_field
"""
import importlib
import json
from pathlib import Path
BRANCH_MODULE = "skills"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
def _import_handler():
"""Import json_handler."""
return importlib.import_module(_json_mod_path)
# ============================================================================
# Group 1 -- Return type contracts
# ============================================================================
def test_handle_command_returns_bool() -> None:
"""handle_command must return a bool (command_returns_bool)."""
from aipass.skills.apps.skills import handle_command
result = handle_command("--help")
assert isinstance(result, bool)
def test_get_json_path_returns_path() -> None:
"""get_json_path must return a Path (paths_return_path contract)."""
handler = _import_handler()
result = handler.get_json_path("contract_mod", "config")
assert isinstance(result, Path)
def test_ensure_json_exists_returns_bool() -> None:
"""ensure_json_exists must return a bool."""
handler = _import_handler()
result = handler.ensure_json_exists("contract_mod", "data")
assert isinstance(result, bool)
assert result is True
def test_load_json_returns_dict_for_config() -> None:
"""load_json for config type must return a dict."""
handler = _import_handler()
result = handler.load_json("contract_mod", "config")
assert isinstance(result, dict)
# ============================================================================
# Group 2 -- Exception contracts
# ============================================================================
def test_save_json_invalid_structure_rejects() -> None:
"""save_json must reject invalid structure -- save_invalid_raises contract."""
handler = _import_handler()
result = handler.save_json("bad", "config", {"missing": "keys"})
assert result is False
def test_validate_rejects_invalid_mode() -> None:
"""validate_json_structure must return False for unknown json_type (invalid_mode_raises)."""
handler = _import_handler()
try:
result = handler.validate_json_structure({}, "invalid_mode_xyz")
except ValueError:
return
assert result is False
def test_save_invalid_raises_no_exception() -> None:
"""save_json with invalid data returns False, no exception (save_invalid_raises)."""
handler = _import_handler()
result = handler.save_json("x", "config", "not_a_dict")
assert result is False
# ============================================================================
# Group 3 -- Data structure contracts
# ============================================================================
def test_config_has_required_keys() -> None:
"""Config must contain module_name and version (config_keys)."""
handler = _import_handler()
handler.ensure_json_exists("struct_mod", "config")
result = handler.load_json("struct_mod", "config")
assert isinstance(result, dict)
assert "module_name" in result
assert "version" in result
def test_data_has_date_keys() -> None:
"""Data structure must contain created and last_updated (data_keys)."""
handler = _import_handler()
handler.ensure_json_exists("struct_mod", "data")
result = handler.load_json("struct_mod", "data")
assert isinstance(result, dict)
assert "created" in result
assert "last_updated" in result
def test_log_entry_has_operation_field() -> None:
"""Log entries must contain an 'operation' field (log_entry_field)."""
handler = _import_handler()
handler.log_operation("contract_test", module_name="struct_mod")
log_path = handler.get_json_path("struct_mod", "log")
log = json.loads(log_path.read_text(encoding="utf-8"))
assert len(log) >= 1
assert "operation" in log[-1]
assert log[-1]["operation"] == "contract_test"
+164
View File
@@ -0,0 +1,164 @@
# =================== AIPass ====================
# Name: test_creator.py
# Description: Tests for creator module orchestration layer
# Version: 1.0.0
# Created: 2026-04-03
# Modified: 2026-04-03
# =============================================
"""
Tests for modules/creator.py — thin orchestration layer.
Covers: handle_command (routing, introspection, --help), create_skill
(delegation to handler, Rich output, trigger firing, json logging),
print_introspection.
"""
from unittest.mock import MagicMock, patch
from aipass.skills.apps.modules.creator import create_skill, handle_command, print_introspection
# ===================================================================
# 1. handle_command — command routing
# ===================================================================
class TestHandleCommand:
"""Tests for handle_command — CLI routing logic."""
def test_no_args_shows_introspection(self, capsys):
result = handle_command("create", [])
assert result is True
output = capsys.readouterr().out
assert "creator Module" in output
def test_help_flag_shows_introspection(self, capsys):
result = handle_command("create", ["--help"])
assert result is True
output = capsys.readouterr().out
assert "creator Module" in output
def test_create_with_valid_name(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
result = handle_command("create", ["test-skill"])
assert result is True
def test_create_with_handler_flag(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
result = handle_command("create", ["test-hnd", "--with-handler"])
assert result is True
skill_path = tmp_path / ".aipass" / "skills" / "test-hnd"
assert (skill_path / "handler.py").exists()
def test_create_with_full_flag(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
result = handle_command("create", ["test-full", "--full"])
assert result is True
skill_path = tmp_path / ".aipass" / "skills" / "test-full"
assert (skill_path / "apps").is_dir()
def test_create_invalid_name_returns_false(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
result = handle_command("create", ["Bad Name!"])
assert result is False
def test_unknown_command_returns_false(self):
result = handle_command("nonexistent", ["arg"])
assert result is False
# ===================================================================
# 2. create_skill — module-level wrapper
# ===================================================================
class TestCreateSkillModule:
"""Tests for create_skill module wrapper — delegates + renders output."""
def test_success_prints_output(self, tmp_path, capsys):
result = create_skill("print-test", template_type="markdown_only", target_dir=tmp_path)
assert result["success"] is True
output = capsys.readouterr().out
assert "print-test" in output
assert "markdown_only" in output
def test_success_returns_handler_result(self, tmp_path):
result = create_skill("result-test", template_type="markdown_only", target_dir=tmp_path)
assert result["success"] is True
assert result["path"] is not None
assert isinstance(result["files"], list)
assert result["error"] is None
def test_failure_does_not_print_success_output(self, capsys):
result = create_skill("", template_type="markdown_only")
assert result["success"] is False
output = capsys.readouterr().out
assert "Created skill" not in output
def test_trigger_fired_on_success(self, tmp_path):
mock_trigger = MagicMock()
with patch("aipass.skills.apps.modules.creator.trigger", mock_trigger):
create_skill("trigger-test", template_type="markdown_only", target_dir=tmp_path)
mock_trigger.fire.assert_called_once()
call_args = mock_trigger.fire.call_args
assert call_args[0][0] == "skill_created"
assert call_args[1]["name"] == "trigger-test"
def test_trigger_not_fired_on_failure(self):
mock_trigger = MagicMock()
with patch("aipass.skills.apps.modules.creator.trigger", mock_trigger):
create_skill("", template_type="markdown_only")
mock_trigger.fire.assert_not_called()
def test_trigger_none_does_not_crash(self, tmp_path):
"""When trigger is None (import failed), create_skill still works."""
with patch("aipass.skills.apps.modules.creator.trigger", None):
result = create_skill("no-trigger", template_type="markdown_only", target_dir=tmp_path)
assert result["success"] is True
@patch("aipass.skills.apps.modules.creator.json_handler")
def test_json_log_on_success(self, mock_jh, tmp_path):
create_skill("jlog-test", template_type="markdown_only", target_dir=tmp_path)
mock_jh.log_operation.assert_called_once()
call_args = mock_jh.log_operation.call_args
assert call_args[0][0] == "skill_created"
assert call_args[0][1]["success"] is True
@patch("aipass.skills.apps.modules.creator.json_handler")
def test_json_log_on_failure(self, mock_jh):
create_skill("", template_type="markdown_only")
mock_jh.log_operation.assert_called_once()
call_args = mock_jh.log_operation.call_args
assert call_args[0][1]["success"] is False
def test_files_listed_in_output(self, tmp_path, capsys):
create_skill("files-test", template_type="with_handler", target_dir=tmp_path)
output = capsys.readouterr().out
assert "SKILL.md" in output
assert "handler.py" in output
# ===================================================================
# 3. print_introspection — module info display
# ===================================================================
class TestPrintIntrospection:
"""Tests for print_introspection — module self-description."""
def test_prints_module_name(self, capsys):
print_introspection()
output = capsys.readouterr().out
assert "creator Module" in output
def test_prints_description(self, capsys):
print_introspection()
output = capsys.readouterr().out
assert "Scaffold" in output
def test_prints_connected_handlers(self, capsys):
print_introspection()
output = capsys.readouterr().out
assert "creator_handler.py" in output
assert "template.py" in output
@@ -0,0 +1,169 @@
# =================== AIPass ====================
# Name: test_creator_handler.py
# Description: Tests for skill creation handler
# Version: 1.0.0
# Created: 2026-04-03
# Modified: 2026-04-03
# =============================================
"""
Tests for creator_handler.py — skill name validation and create_skill logic.
Covers: is_valid_name, create_skill (success paths, validation failures,
template failures, target_dir default, json logging).
"""
import sys
from pathlib import Path
from unittest.mock import patch
from aipass.skills.apps.handlers.creator_handler import create_skill, is_valid_name
# ===================================================================
# 1. is_valid_name — name validation
# ===================================================================
class TestIsValidName:
"""Tests for is_valid_name — skill name validation rules."""
def test_simple_lowercase_name(self):
assert is_valid_name("my-skill") is True
def test_single_letter(self):
assert is_valid_name("a") is True
def test_lowercase_with_numbers(self):
assert is_valid_name("skill2") is True
def test_underscores_allowed(self):
assert is_valid_name("my_skill") is True
def test_hyphens_allowed(self):
assert is_valid_name("my-skill") is True
def test_mixed_separators(self):
assert is_valid_name("my-skill_v2") is True
def test_rejects_empty_string(self):
assert is_valid_name("") is False
def test_rejects_none(self):
"""None is falsy — short-circuits to False via 'not name'."""
assert is_valid_name(None) is False
def test_rejects_starts_with_number(self):
assert is_valid_name("2skill") is False
def test_rejects_starts_with_hyphen(self):
assert is_valid_name("-skill") is False
def test_rejects_uppercase(self):
assert is_valid_name("MySkill") is False
def test_rejects_mixed_case(self):
assert is_valid_name("mySkill") is False
def test_rejects_spaces(self):
assert is_valid_name("my skill") is False
def test_rejects_special_chars(self):
assert is_valid_name("my.skill") is False
def test_rejects_slash(self):
assert is_valid_name("my/skill") is False
# ===================================================================
# 2. create_skill — skill creation orchestration
# ===================================================================
class TestCreateSkill:
"""Tests for create_skill — full creation pipeline."""
def test_create_markdown_skill_succeeds(self, tmp_path):
result = create_skill("test-md", template_type="markdown_only", target_dir=tmp_path)
assert result["success"] is True
assert result["path"] is not None
assert Path(result["path"]).exists()
assert (Path(result["path"]) / "SKILL.md").exists()
def test_create_handler_skill_succeeds(self, tmp_path):
result = create_skill("test-hnd", template_type="with_handler", target_dir=tmp_path)
assert result["success"] is True
assert (Path(result["path"]) / "handler.py").exists()
def test_create_full_skill_succeeds(self, tmp_path):
result = create_skill("test-full", template_type="full", target_dir=tmp_path)
assert result["success"] is True
assert (Path(result["path"]) / "apps").is_dir()
def test_returns_created_files_list(self, tmp_path):
result = create_skill("test-files", template_type="markdown_only", target_dir=tmp_path)
assert isinstance(result["files"], list)
assert len(result["files"]) > 0
assert "SKILL.md" in result["files"]
def test_empty_name_fails(self):
result = create_skill("", template_type="markdown_only")
assert result["success"] is False
assert result["error"] == "Skill name is required."
assert result["path"] is None
assert result["files"] == []
def test_invalid_name_fails(self):
result = create_skill("Bad Name!", template_type="markdown_only")
assert result["success"] is False
assert "Invalid skill name" in result["error"]
def test_invalid_template_type_fails(self, tmp_path):
result = create_skill("valid-name", template_type="nonexistent", target_dir=tmp_path)
assert result["success"] is False
assert "Unknown template type" in result["error"]
def test_default_target_dir_uses_cwd(self, monkeypatch, tmp_path):
"""When target_dir is None, uses CWD/.aipass/skills/."""
monkeypatch.chdir(tmp_path)
result = create_skill("cwd-test", template_type="markdown_only")
assert result["success"] is True
expected_parent = tmp_path / ".aipass" / "skills"
assert str(expected_parent) in result["path"]
def test_duplicate_name_fails(self, tmp_path):
"""Creating a skill that already exists should fail."""
create_skill("dupe-test", template_type="markdown_only", target_dir=tmp_path)
result = create_skill("dupe-test", template_type="markdown_only", target_dir=tmp_path)
assert result["success"] is False
assert "already exists" in result["error"]
def test_placeholder_replacement(self, tmp_path):
"""Skill name replaces {{SKILL_NAME}} in created files."""
result = create_skill("my-replaced", template_type="markdown_only", target_dir=tmp_path)
content = (Path(result["path"]) / "SKILL.md").read_text()
assert "my-replaced" in content
assert "{{SKILL_NAME}}" not in content
def test_logs_json_operation_on_success(self, tmp_path):
_mod = sys.modules["aipass.skills.apps.handlers.creator_handler"]
with patch.object(_mod, "json_handler") as mock_jh:
create_skill("log-test", template_type="markdown_only", target_dir=tmp_path)
mock_jh.log_operation.assert_called_once()
call_args = mock_jh.log_operation.call_args
assert call_args[0][0] == "skill_scaffold"
assert call_args[0][1]["success"] is True
def test_logs_json_operation_on_failure(self, tmp_path):
_mod = sys.modules["aipass.skills.apps.handlers.creator_handler"]
# Use a duplicate-name scenario so validation passes but copy fails,
# which is the only failure path that reaches json_handler.log_operation.
create_skill("dup-log", template_type="markdown_only", target_dir=tmp_path)
with patch.object(_mod, "json_handler") as mock_jh:
create_skill("dup-log", template_type="markdown_only", target_dir=tmp_path)
mock_jh.log_operation.assert_called_once()
call_args = mock_jh.log_operation.call_args
assert call_args[0][1]["success"] is False
+204
View File
@@ -0,0 +1,204 @@
# ===================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 tempfile
from pathlib import Path
from aipass.skills.apps.handlers.discovery_handler 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 is not None
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 is not None
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
@@ -0,0 +1,98 @@
# =================== AIPass ====================
# Name: test_error_resilience.py
# Description: Error Resilience Tests for skills branch
# Version: 1.0.0
# Created: 2026-03-28
# Modified: 2026-03-28
# =============================================
"""
Error Resilience Tests for skills branch.
Covers 4 tests:
- missing_file, corrupt_json, empty_file, nonexistent_dir
"""
import importlib
import json
from pathlib import Path
BRANCH_MODULE = "aipass.skills"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
def _import_handler():
"""Import json_handler."""
return importlib.import_module(_json_mod_path)
# ============================================================================
# Error Resilience Tests
# ============================================================================
def test_missing_file() -> None:
"""Loading a non-existent file returns a graceful default, not a crash."""
handler = _import_handler()
target = handler.get_json_path("ghost", "config")
assert not target.exists()
try:
result = handler.load_json("ghost", "config")
except FileNotFoundError:
return
assert result is not None
assert isinstance(result, dict)
def test_corrupt_json() -> None:
"""Corrupt JSON on disk is handled gracefully -- file is regenerated."""
handler = _import_handler()
json_dir = handler.SKILLS_JSON_DIR
json_dir.mkdir(parents=True, exist_ok=True)
target = handler.get_json_path("corrupt", "data")
target.write_bytes(b"\x00\x01NOT-JSON{{{broken")
result = handler.ensure_json_exists("corrupt", "data")
assert result is True
raw = target.read_text(encoding="utf-8")
data = json.loads(raw)
assert isinstance(data, dict)
assert "created" in data
assert "last_updated" in data
def test_empty_file() -> None:
"""An empty file (0 bytes) is handled gracefully."""
handler = _import_handler()
json_dir = handler.SKILLS_JSON_DIR
json_dir.mkdir(parents=True, exist_ok=True)
target = handler.get_json_path("empty", "log")
target.write_text("", encoding="utf-8")
result = handler.ensure_json_exists("empty", "log")
assert result is True
raw = target.read_text(encoding="utf-8")
data = json.loads(raw)
assert isinstance(data, list)
def test_nonexistent_dir(tmp_path: Path) -> None:
"""Missing parent directory is handled gracefully."""
handler = _import_handler()
from unittest.mock import patch
nested_dir = tmp_path / "does_not_exist" / "nested"
assert not nested_dir.exists()
with patch.object(handler, "SKILLS_JSON_DIR", nested_dir):
try:
result = handler.ensure_json_exists("nodir", "config")
assert nested_dir.exists()
assert result is True
except (FileNotFoundError, OSError):
pass
@@ -0,0 +1,108 @@
# =================== AIPass ====================
# Name: test_init_provisioning.py
# Description: Init/Provisioning Tests for skills branch
# Version: 1.0.0
# Created: 2026-03-28
# Modified: 2026-03-28
# =============================================
"""
Init/Provisioning Tests for skills branch.
Covers 4 tests:
- creates_files, auto_creates_dir, no_overwrite, returns_dict
"""
import importlib
import json
from pathlib import Path
from unittest.mock import patch
import pytest
BRANCH_MODULE = "skills"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
def _import_handler():
"""Import json_handler."""
return importlib.import_module(_json_mod_path)
# ============================================================================
# Init/Provisioning Tests
# ============================================================================
def test_creates_expected_files() -> None:
"""ensure_json_exists creates expected files on disk."""
handler = _import_handler()
json_dir = handler.SKILLS_JSON_DIR
for json_type in ("config", "data", "log"):
result = handler.ensure_json_exists("prov_mod", json_type)
assert result is True
expected = json_dir / f"prov_mod_{json_type}.json"
assert expected.exists()
raw = expected.read_text(encoding="utf-8")
parsed = json.loads(raw)
assert parsed is not None
def test_auto_creates_directory(tmp_path: Path) -> None:
"""ensure_json_exists auto-creates parent directory when missing."""
handler = _import_handler()
nested_dir = tmp_path / "auto_created" / "subdir"
assert not nested_dir.exists()
with patch.object(handler, "SKILLS_JSON_DIR", nested_dir):
try:
result = handler.ensure_json_exists("autodir", "config")
assert nested_dir.exists()
assert result is True
assert (nested_dir / "autodir_config.json").exists()
except (FileNotFoundError, OSError):
pytest.skip("Branch does not auto-create missing directories")
def test_no_overwrite_on_second_call() -> None:
"""Second call must not overwrite existing data (no_overwrite idempotency)."""
handler = _import_handler()
json_dir = handler.SKILLS_JSON_DIR
json_dir.mkdir(parents=True, exist_ok=True)
handler.ensure_json_exists("idem_mod", "data")
target = json_dir / "idem_mod_data.json"
original = json.loads(target.read_text(encoding="utf-8"))
original["custom_field"] = "do_not_overwrite"
target.write_text(json.dumps(original, indent=2), encoding="utf-8")
handler.ensure_json_exists("idem_mod", "data")
after = json.loads(target.read_text(encoding="utf-8"))
assert after.get("custom_field") == "do_not_overwrite"
def test_returns_dict_with_expected_keys() -> None:
"""Provisioned files contain the correct structure keys."""
handler = _import_handler()
handler.ensure_json_exists("key_mod", "config")
config = handler.load_json("key_mod", "config")
assert isinstance(config, dict)
assert "module_name" in config
assert "version" in config
handler.ensure_json_exists("key_mod", "data")
data = handler.load_json("key_mod", "data")
assert isinstance(data, dict)
assert "created" in data
assert "last_updated" in data
handler.ensure_json_exists("key_mod", "log")
log = handler.load_json("key_mod", "log")
assert isinstance(log, list)
@@ -0,0 +1,285 @@
# =================== AIPass ====================
# Name: test_json_handler.py
# Description: Tests for skills JSON handler
# Version: 1.0.0
# Created: 2026-03-28
# Modified: 2026-03-28
# =============================================
"""
Tests for skills JSON handler -- auto-creating JSON system.
Covers json_handler.py functions: validate_json_structure, get_json_path,
ensure_json_exists, load_json, save_json, _get_default, ensure_module_jsons,
log_operation.
"""
import importlib
import json
from pathlib import Path
from unittest.mock import patch
import pytest
# ---------------------------------------------------------------------------
# Import helper
# ---------------------------------------------------------------------------
BRANCH_MODULE = "skills"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
def _import_handler():
"""Import json_handler inside test so autouse mocks are active."""
return importlib.import_module(_json_mod_path)
@pytest.fixture()
def sample_data():
"""Sample test data for JSON operations."""
return {
"config": {
"module_name": "test_module",
"version": "1.0.0",
"config": {"max_log_entries": 50},
"timestamp": "2026-03-28",
},
"data": {
"module_name": "test_module",
"created": "2026-03-28",
"last_updated": "2026-03-28",
"operations_total": 0,
"operations_successful": 0,
"operations_failed": 0,
},
"log": [{"timestamp": "2026-03-28T10:00:00", "operation": "test"}],
}
# ===================================================================
# 1. _get_default -- default factory for JSON types
# ===================================================================
class TestDefaultFactory:
"""Tests for _get_default template default_factory."""
def test_config_default_factory_has_module_name(self):
handler = _import_handler()
result = handler._get_default("config", "test_mod")
assert result["module_name"] == "test_mod"
def test_config_default_factory_has_required_keys(self):
handler = _import_handler()
result = handler._get_default("config", "test_mod")
assert "module_name" in result
assert "version" in result
assert "config" in result
def test_data_default_factory_has_dates(self):
handler = _import_handler()
result = handler._get_default("data", "test_mod")
assert "created" in result
assert "last_updated" in result
def test_log_default_factory_is_list(self):
handler = _import_handler()
result = handler._get_default("log", "test_mod")
assert isinstance(result, list)
assert len(result) == 0
def test_unknown_type_default_factory_returns_none(self):
handler = _import_handler()
result = handler._get_default("nonexistent", "test_mod")
assert result is None
# ===================================================================
# 2. validate_json_structure
# ===================================================================
class TestValidate:
"""Tests for validate_json_structure -- validate."""
def test_validate_valid_config(self, sample_data):
handler = _import_handler()
assert handler.validate_json_structure(sample_data["config"], "config") is True
def test_validate_valid_data(self, sample_data):
handler = _import_handler()
assert handler.validate_json_structure(sample_data["data"], "data") is True
def test_validate_valid_log(self, sample_data):
handler = _import_handler()
assert handler.validate_json_structure(sample_data["log"], "log") is True
def test_validate_invalid_config_missing_keys(self):
handler = _import_handler()
assert handler.validate_json_structure({"only": "partial"}, "config") is False
def test_validate_config_non_dict_fails(self):
handler = _import_handler()
assert handler.validate_json_structure("not a dict", "config") is False
def test_validate_unknown_type_fails(self):
handler = _import_handler()
assert handler.validate_json_structure({}, "unknown_type") is False
def test_validate_log_non_list_fails(self):
handler = _import_handler()
assert handler.validate_json_structure({"not": "a list"}, "log") is False
# ===================================================================
# 3. get_json_path -- get_path
# ===================================================================
class TestGetPath:
"""Tests for get_json_path -- get_path."""
def test_get_path_returns_path_type(self):
handler = _import_handler()
result = handler.get_json_path("test_mod", "config")
assert isinstance(result, Path)
def test_get_path_contains_module_and_type(self):
handler = _import_handler()
result = handler.get_json_path("my_module", "data")
assert result.name == "my_module_data.json"
def test_get_path_in_skills_json_dir(self):
handler = _import_handler()
result = handler.get_json_path("mod", "log")
assert "skills_json" in str(result) or result.parent == handler.SKILLS_JSON_DIR
# ===================================================================
# 4. ensure_json_exists -- ensure_exists
# ===================================================================
class TestEnsureExists:
"""Tests for ensure_json_exists -- ensure_exists."""
def test_ensure_exists_creates_new_file(self):
handler = _import_handler()
result = handler.ensure_json_exists("test", "config")
assert result is True
def test_ensure_exists_auto_creates_dir(self, tmp_path):
handler = _import_handler()
new_dir = tmp_path / "new_subdir"
with patch.object(handler, "SKILLS_JSON_DIR", new_dir):
result = handler.ensure_json_exists("test", "config")
assert result is True
assert new_dir.exists()
def test_ensure_exists_returns_false_for_unknown_type(self):
handler = _import_handler()
result = handler.ensure_json_exists("test", "nonexistent")
assert result is False
# ===================================================================
# 5. load_json -- load
# ===================================================================
class TestLoad:
"""Tests for load_json -- load."""
def test_load_config_returns_dict(self):
handler = _import_handler()
result = handler.load_json("t", "config")
assert isinstance(result, dict)
def test_load_log_returns_list(self):
handler = _import_handler()
result = handler.load_json("t", "log")
assert isinstance(result, list)
def test_load_returns_none_for_bad_type(self):
handler = _import_handler()
result = handler.load_json("t", "nonexistent")
assert result is None
# ===================================================================
# 6. save_json -- save
# ===================================================================
class TestSave:
"""Tests for save_json -- save."""
def test_save_valid_config(self, sample_data):
handler = _import_handler()
handler.ensure_json_exists("test", "config")
result = handler.save_json("test", "config", sample_data["config"])
assert result is True
def test_save_invalid_structure_returns_false(self):
"""save_json rejects invalid data."""
handler = _import_handler()
result = handler.save_json("test", "config", {"bad": "structure"})
assert result is False
def test_save_updates_last_updated_for_data(self, sample_data):
handler = _import_handler()
handler.ensure_json_exists("test", "data")
handler.save_json("test", "data", sample_data["data"])
json_path = handler.get_json_path("test", "data")
saved = json.loads(json_path.read_text(encoding="utf-8"))
assert "last_updated" in saved
# ===================================================================
# 7. log_operation
# ===================================================================
class TestLogOperation:
"""Tests for log_operation."""
def test_log_operation_creates_entry(self):
handler = _import_handler()
result = handler.log_operation("test_op", module_name="test_mod")
assert result is True
def test_log_operation_entry_has_operation_field(self):
handler = _import_handler()
handler.log_operation("my_op", module_name="log_mod")
log = handler.load_json("log_mod", "log")
assert len(log) >= 1
assert "operation" in log[-1]
assert log[-1]["operation"] == "my_op"
def test_log_operation_with_data(self):
handler = _import_handler()
handler.log_operation("data_op", data={"key": "value"}, module_name="log_mod2")
log = handler.load_json("log_mod2", "log")
assert log[-1]["data"]["key"] == "value"
# ===================================================================
# 8. ensure_module_jsons -- ensure_module
# ===================================================================
class TestEnsureModule:
"""Tests for ensure_module_jsons -- ensure_module."""
def test_ensure_module_returns_true(self):
handler = _import_handler()
result = handler.ensure_module_jsons("test_mod")
assert result is True
def test_ensure_module_creates_all_three(self):
handler = _import_handler()
handler.ensure_module_jsons("full_mod")
for json_type in ("config", "data", "log"):
path = handler.get_json_path("full_mod", json_type)
assert path.exists()
+208
View File
@@ -0,0 +1,208 @@
# ===================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
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from aipass.skills.apps.handlers.template import copy_template, get_template # noqa: E402
from aipass.skills.apps.modules.creator import create_skill # noqa: E402
from aipass.skills.apps.modules.discovery import discover_skills_in_path, parse_frontmatter # noqa: E402, F401
from aipass.skills.apps.handlers.loader_handler import import_handler, parse_full_skill_md # noqa: E402
from aipass.skills.apps.modules.runner import run_skill # noqa: E402
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)
result = parse_full_skill_md(skill_path / "SKILL.md")
metadata, body = result[0], result[1]
assert metadata is not None
assert isinstance(metadata, dict)
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 = import_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
result = parse_full_skill_md(github[0]["path"] / "SKILL.md")
metadata, body = result[0], result[1]
assert metadata is not None
assert isinstance(metadata, dict)
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)
def test_copy_template_excludes_pycache(self):
"""copy_template must not include __pycache__ directories in output."""
tmpdir = tempfile.mkdtemp()
try:
template = get_template("full")
assert template["success"] is True
# Create a __pycache__ dir inside the template to ensure it gets filtered
pycache = template["path"] / "__pycache__"
pycache_existed = pycache.exists()
if not pycache_existed:
pycache.mkdir()
(pycache / "dummy.pyc").write_bytes(b"\x00")
try:
target = Path(tmpdir) / "cache-test"
result = copy_template(template["path"], target, "cache-test")
assert result["success"] is True
assert not (target / "__pycache__").exists()
for f in result["created_files"]:
assert "__pycache__" not in f
finally:
if not pycache_existed:
shutil.rmtree(str(pycache))
finally:
shutil.rmtree(tmpdir)
+70
View File
@@ -0,0 +1,70 @@
# ===================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."""
from aipass.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
# Verify actual values, not just key existence
assert metadata["name"] == "github"
assert isinstance(metadata["description"], str)
assert len(metadata["description"]) > 0
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
+165
View File
@@ -0,0 +1,165 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: test_registry.py - Unit tests for skills registry
# Date: 2026-03-10
# Version: 1.0.0
# Category: skills/tests
# =============================================
"""Tests for the skills registry handler."""
import sys
import tempfile
from pathlib import Path
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from aipass.skills.apps.handlers.registry import build_registry, get_skill, get_skill_names # noqa: E402
class TestBuildRegistry:
def _make_discover_fn(self, skills_by_path):
"""Helper: returns a discover_fn that returns skills based on path."""
def discover_fn(path, source_label):
return skills_by_path.get(str(path), [])
return discover_fn
def test_empty_search_paths(self):
registry = build_registry([], lambda p, s: [])
assert registry == []
def test_nonexistent_path_skipped(self):
def discover_fn(p, s):
return [{"name": "should-not-appear"}]
registry = build_registry(
[("/nonexistent/path/xyz_abc_123", "test")],
discover_fn,
)
assert registry == []
def test_discovers_skills_from_valid_path(self):
with tempfile.TemporaryDirectory() as tmpdir:
skill = {"name": "alpha", "description": "A skill", "source": "test"}
discover_fn = self._make_discover_fn({tmpdir: [skill]})
registry = build_registry([(tmpdir, "test")], discover_fn)
assert len(registry) == 1
assert registry[0]["name"] == "alpha"
assert registry[0]["description"] == "A skill"
def test_first_match_wins_dedup(self):
"""When two paths contain a skill with the same name, first path wins."""
with tempfile.TemporaryDirectory() as dir1, tempfile.TemporaryDirectory() as dir2:
skill_v1 = {"name": "dupe", "description": "First", "source": "project"}
skill_v2 = {"name": "dupe", "description": "Second", "source": "builtin"}
discover_fn = self._make_discover_fn(
{
dir1: [skill_v1],
dir2: [skill_v2],
}
)
registry = build_registry(
[(dir1, "project"), (dir2, "builtin")],
discover_fn,
)
assert len(registry) == 1
assert registry[0]["description"] == "First"
assert registry[0]["source"] == "project"
def test_different_names_both_included(self):
with tempfile.TemporaryDirectory() as dir1, tempfile.TemporaryDirectory() as dir2:
skill_a = {"name": "alpha", "description": "A"}
skill_b = {"name": "beta", "description": "B"}
discover_fn = self._make_discover_fn(
{
dir1: [skill_a],
dir2: [skill_b],
}
)
registry = build_registry(
[(dir1, "project"), (dir2, "builtin")],
discover_fn,
)
assert len(registry) == 2
names = {s["name"] for s in registry}
assert names == {"alpha", "beta"}
def test_multiple_skills_from_single_path(self):
with tempfile.TemporaryDirectory() as tmpdir:
skills = [
{"name": "one", "description": "First"},
{"name": "two", "description": "Second"},
{"name": "three", "description": "Third"},
]
discover_fn = self._make_discover_fn({tmpdir: skills})
registry = build_registry([(tmpdir, "test")], discover_fn)
assert len(registry) == 3
def test_discover_fn_is_called_with_path_and_label(self):
"""Verify discover_fn receives Path object and source label."""
calls = []
def tracking_fn(path, source_label):
calls.append((path, source_label))
return []
with tempfile.TemporaryDirectory() as tmpdir:
build_registry([(tmpdir, "my_source")], tracking_fn)
assert len(calls) == 1
assert isinstance(calls[0][0], Path)
assert calls[0][1] == "my_source"
class TestGetSkill:
def test_found(self):
registry = [
{"name": "alpha", "description": "A"},
{"name": "beta", "description": "B"},
]
result = get_skill("beta", registry)
assert result is not None
assert result["name"] == "beta"
assert result["description"] == "B"
def test_not_found(self):
registry = [{"name": "alpha", "description": "A"}]
result = get_skill("nonexistent", registry)
assert result is None
def test_empty_registry(self):
result = get_skill("anything", [])
assert result is None
def test_returns_first_match(self):
"""If registry somehow has duplicates, returns the first one."""
registry = [
{"name": "dup", "description": "First"},
{"name": "dup", "description": "Second"},
]
result = get_skill("dup", registry)
assert result is not None
assert result["description"] == "First"
class TestGetSkillNames:
def test_returns_sorted_names(self):
registry = [
{"name": "charlie"},
{"name": "alpha"},
{"name": "bravo"},
]
names = get_skill_names(registry)
assert names == ["alpha", "bravo", "charlie"]
def test_empty_registry(self):
names = get_skill_names([])
assert names == []
def test_single_skill(self):
registry = [{"name": "only"}]
names = get_skill_names(registry)
assert names == ["only"]
+101
View File
@@ -0,0 +1,101 @@
# ===================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."""
from aipass.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
# Verify values are correct, not just keys
assert result["success"] is True
assert "Disk Usage" in result["output"]
assert result["error"] is None
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
# Content assertions — not just types
assert result["success"] is True
assert len(result["output"]) > 0
assert "Disk Usage" in result["output"]
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)
# Content assertions — not just types
assert result["success"] is False
assert "not found" in result["error"].lower()
assert result["output"] == ""
@@ -0,0 +1,118 @@
# ===================AIPASS====================
# META DATA HEADER
# Name: test_runner_handler.py - Unit tests for runner_handler (empty body, etc.)
# Date: 2026-03-10
# Version: 1.0.0
# Category: skills/tests
# =============================================
"""Tests for the skills runner handler, focusing on run_markdown edge cases."""
import sys
from pathlib import Path
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from aipass.skills.apps.handlers.runner_handler import run_markdown, run_handler # noqa: E402
class TestRunMarkdownEmptyBody:
def test_empty_body_returns_success(self):
result = run_markdown("empty-skill", {"description": "test"}, "")
assert result["success"] is True
def test_empty_body_output_mentions_no_instructions(self):
result = run_markdown("empty-skill", {}, "")
assert "no instructions body" in result["output"].lower()
assert "empty-skill" in result["output"]
def test_none_body_returns_no_instructions(self):
result = run_markdown("test-skill", {}, None)
assert result["success"] is True
assert "no instructions body" in result["output"].lower()
def test_empty_body_no_error(self):
result = run_markdown("test-skill", {}, "")
assert result["error"] is None
class TestRunMarkdownWithBody:
def test_body_included_in_output(self):
result = run_markdown("my-skill", {"description": "A skill"}, "# Instructions\nDo stuff.")
assert result["success"] is True
assert "# Instructions" in result["output"]
assert "Do stuff." in result["output"]
def test_header_includes_skill_name(self):
result = run_markdown("my-skill", {}, "body content")
assert "=== Skill: my-skill ===" in result["output"]
def test_header_includes_description(self):
result = run_markdown("my-skill", {"description": "Does things"}, "body")
assert "Does things" in result["output"]
def test_no_description_still_works(self):
result = run_markdown("my-skill", {}, "body")
assert result["success"] is True
assert "=== Skill: my-skill ===" in result["output"]
class TestRunHandler:
def test_no_action_with_get_actions(self):
"""When action is None and handler has get_actions, list them."""
class MockHandler:
def get_actions(self):
return ["disk", "memory"]
result = run_handler(MockHandler(), "test-skill", None, {}, {})
assert result["success"] is True
assert "disk" in result["output"]
assert "memory" in result["output"]
def test_no_action_without_get_actions(self):
"""When action is None and handler lacks get_actions, return error."""
class MockHandler:
pass
result = run_handler(MockHandler(), "test-skill", None, {}, {})
assert result["success"] is False
assert "no action specified" in result["error"].lower()
def test_handler_no_run_function(self):
class MockHandler:
pass
result = run_handler(MockHandler(), "test-skill", "do_stuff", {}, {})
assert result["success"] is False
assert "no run() function" in result["error"].lower()
def test_handler_returns_dict(self):
class MockHandler:
def run(self, action, args=None, config=None):
return {"success": True, "output": "done", "error": None}
result = run_handler(MockHandler(), "test-skill", "go", {}, {})
assert result["success"] is True
assert result["output"] == "done"
def test_handler_returns_non_dict(self):
class MockHandler:
def run(self, action, args=None, config=None):
return "just a string"
result = run_handler(MockHandler(), "test-skill", "go", {}, {})
assert result["success"] is True
assert result["output"] == "just a string"
def test_handler_raises_exception(self):
class MockHandler:
def run(self, action, args=None, config=None):
raise ValueError("boom")
result = run_handler(MockHandler(), "test-skill", "go", {}, {})
assert result["success"] is False
assert "boom" in result["error"]
+235
View File
@@ -0,0 +1,235 @@
# =================== AIPass ====================
# Name: test_template.py
# Description: Tests for skill template management
# Version: 1.0.0
# Created: 2026-04-03
# Modified: 2026-04-03
# =============================================
"""
Tests for template.py — template resolution, placeholder replacement, copy logic.
Covers: get_template, _replace_placeholder_in_file, copy_template
(valid/invalid types, placeholder replacement, binary skip, error paths,
target exists, cleanup on failure, __pycache__ exclusion).
"""
import shutil
import sys
from pathlib import Path
from unittest.mock import patch
from aipass.skills.apps.handlers.template import (
TEMPLATES_DIR,
VALID_TYPES,
_replace_placeholder_in_file,
copy_template,
get_template,
)
# ===================================================================
# 1. get_template — template path resolution
# ===================================================================
class TestGetTemplate:
"""Tests for get_template — resolve template directories."""
def test_markdown_only_returns_valid_path(self):
result = get_template("markdown_only")
assert result["success"] is True
assert result["path"].exists()
assert result["path"].is_dir()
assert result["error"] is None
def test_with_handler_returns_valid_path(self):
result = get_template("with_handler")
assert result["success"] is True
assert result["path"].exists()
def test_full_returns_valid_path(self):
result = get_template("full")
assert result["success"] is True
assert result["path"].exists()
def test_invalid_type_fails(self):
result = get_template("bogus")
assert result["success"] is False
assert result["path"] is None
assert "Unknown template type" in result["error"]
assert "bogus" in result["error"]
def test_error_lists_valid_types(self):
result = get_template("wrong")
for vt in VALID_TYPES:
assert vt in result["error"]
def test_missing_directory_fails(self, monkeypatch):
"""If template dir doesn't exist on disk, should fail gracefully."""
_tpl_mod = sys.modules["aipass.skills.apps.handlers.template"]
monkeypatch.setattr(
_tpl_mod,
"TEMPLATES_DIR",
Path("/nonexistent/templates"),
)
result = get_template("markdown_only")
assert result["success"] is False
assert "not found" in result["error"]
def test_templates_dir_points_to_real_directory(self):
assert TEMPLATES_DIR.exists()
assert TEMPLATES_DIR.is_dir()
def test_all_valid_types_have_directories(self):
for vt in VALID_TYPES:
assert (TEMPLATES_DIR / vt).exists(), f"Missing template dir: {vt}"
# ===================================================================
# 2. _replace_placeholder_in_file — in-file substitution
# ===================================================================
class TestReplacePlaceholder:
"""Tests for _replace_placeholder_in_file — {{SKILL_NAME}} replacement."""
def test_replaces_placeholder_in_text(self, tmp_path):
f = tmp_path / "test.md"
f.write_text("name: {{SKILL_NAME}}\ndesc: {{SKILL_NAME}} is great")
_replace_placeholder_in_file(f, "my-tool")
content = f.read_text()
assert "my-tool" in content
assert "{{SKILL_NAME}}" not in content
def test_no_placeholder_leaves_file_unchanged(self, tmp_path):
f = tmp_path / "noop.txt"
original = "no placeholders here"
f.write_text(original)
_replace_placeholder_in_file(f, "anything")
assert f.read_text() == original
def test_skips_binary_file(self, tmp_path):
"""Binary files with UnicodeDecodeError should be silently skipped."""
f = tmp_path / "binary.bin"
f.write_bytes(b"\x80\x81\x82\xff{{SKILL_NAME}}")
# Should not raise
_replace_placeholder_in_file(f, "test")
# File should still be binary (unchanged or at least not crash)
assert f.exists()
def test_empty_file_no_error(self, tmp_path):
f = tmp_path / "empty.md"
f.write_text("")
_replace_placeholder_in_file(f, "test")
assert f.read_text() == ""
def test_multiple_placeholders_all_replaced(self, tmp_path):
f = tmp_path / "multi.md"
f.write_text("A={{SKILL_NAME}} B={{SKILL_NAME}} C={{SKILL_NAME}}")
_replace_placeholder_in_file(f, "x")
content = f.read_text()
assert content == "A=x B=x C=x"
# ===================================================================
# 3. copy_template — full template copy pipeline
# ===================================================================
class TestCopyTemplate:
"""Tests for copy_template — copy + placeholder replacement."""
def test_copy_markdown_template(self, tmp_path):
src = get_template("markdown_only")
target = tmp_path / "new-skill"
result = copy_template(src["path"], target, "new-skill")
assert result["success"] is True
assert target.exists()
assert len(result["created_files"]) > 0
assert result["error"] is None
def test_created_files_are_sorted(self, tmp_path):
src = get_template("with_handler")
target = tmp_path / "sorted-test"
result = copy_template(src["path"], target, "sorted-test")
assert result["created_files"] == sorted(result["created_files"])
def test_placeholders_replaced_in_all_files(self, tmp_path):
src = get_template("with_handler")
target = tmp_path / "placeholder-test"
copy_template(src["path"], target, "placeholder-test")
for f in target.rglob("*"):
if f.is_file():
try:
content = f.read_text(encoding="utf-8")
assert "{{SKILL_NAME}}" not in content, f"Unreplaced in {f.name}"
except UnicodeDecodeError:
pass # skip binary
def test_target_already_exists_fails(self, tmp_path):
target = tmp_path / "exists"
target.mkdir()
src = get_template("markdown_only")
result = copy_template(src["path"], target, "exists")
assert result["success"] is False
assert "already exists" in result["error"]
assert result["created_files"] == []
def test_invalid_source_fails(self, tmp_path):
target = tmp_path / "bad-src"
result = copy_template(Path("/nonexistent/template"), target, "bad")
assert result["success"] is False
assert "Failed to create skill" in result["error"]
def test_cleanup_on_failure(self, tmp_path):
"""If copy fails mid-way, target dir should be cleaned up."""
target = tmp_path / "cleanup-test"
result = copy_template(Path("/nonexistent"), target, "test")
assert result["success"] is False
# Target should not exist after cleanup
assert not target.exists()
def test_pycache_excluded(self, tmp_path):
"""__pycache__ directories must not appear in output."""
src = get_template("full")
assert src["success"]
# Inject a __pycache__ into the template temporarily
pycache = src["path"] / "__pycache__"
created = False
if not pycache.exists():
pycache.mkdir()
(pycache / "cached.pyc").write_bytes(b"\x00")
created = True
try:
target = tmp_path / "no-cache"
result = copy_template(src["path"], target, "no-cache")
assert result["success"] is True
assert not (target / "__pycache__").exists()
for f in result["created_files"]:
assert "__pycache__" not in f
finally:
if created:
shutil.rmtree(str(pycache))
def test_full_template_has_apps_structure(self, tmp_path):
src = get_template("full")
target = tmp_path / "full-test"
result = copy_template(src["path"], target, "full-test")
assert result["success"] is True
assert (target / "apps").is_dir()
assert (target / "apps" / "modules").is_dir()
assert (target / "apps" / "handlers").is_dir()
def test_logs_template_copied_operation(self, tmp_path):
_tpl_mod = sys.modules["aipass.skills.apps.handlers.template"]
with patch.object(_tpl_mod, "json_handler") as mock_jh:
src = get_template("markdown_only")
target = tmp_path / "log-test"
copy_template(src["path"], target, "log-test")
mock_jh.log_operation.assert_called_once()
call_args = mock_jh.log_operation.call_args
assert call_args[0][0] == "template_copied"
assert call_args[0][1]["files_count"] > 0
+111
View File
@@ -0,0 +1,111 @@
# ===================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
skills_root = Path(__file__).resolve().parent.parent.parent
if str(skills_root) not in sys.path:
sys.path.insert(0, str(skills_root))
from aipass.skills.apps.handlers.validator import validate_skill # noqa: E402
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
# Verify actual values, not just key existence
assert result["valid"] is True
assert result["missing_pip"] == []
assert result["missing_bins"] == []
assert result["missing_config"] == []
def test_return_structure_with_failures(self):
"""Verify structure contains actual failure data, not just keys."""
result = validate_skill(
{
"requires": {
"pip": ["nonexistent_pkg_xyz_123"],
"bins": ["nonexistent_bin_xyz"],
"config": ["NONEXISTENT_VAR_XYZ"],
}
}
)
assert result["valid"] is False
assert result["missing_pip"] == ["nonexistent_pkg_xyz_123"]
assert result["missing_bins"] == ["nonexistent_bin_xyz"]
assert result["missing_config"] == ["NONEXISTENT_VAR_XYZ"]