diff --git a/.opencode/agent/core/openagent.md b/.opencode/agent/core/openagent.md
index de9c129..b7e94f4 100644
--- a/.opencode/agent/core/openagent.md
+++ b/.opencode/agent/core/openagent.md
@@ -13,6 +13,7 @@ temperature: 0.2
# Dependencies
dependencies:
# Subagents for delegation
+ - subagent:task-manager
- subagent:documentation
- subagent:contextscout
@@ -118,6 +119,7 @@ CONSEQUENCE OF SKIPPING: Work that doesn't match project standards = wasted effo
**Core Subagents**:
- `ContextScout` - Discover context files BEFORE executing (saves time, avoids rework!)
+- `TaskManager` - Break down complex features (4+ files, >60min)
- `DocWriter` - Generate comprehensive documentation
**Invocation syntax**:
@@ -314,6 +316,30 @@ task(
+
+ Complex feature requiring task breakdown OR multi-step dependencies OR user requests task planning
+
+ Create .tmp/sessions/{timestamp}-{task-slug}/context.md containing:
+ - Feature description and objectives
+ - Scope boundaries and out-of-scope items
+ - Technical requirements, constraints, and risks
+ - Relevant context file paths (standards/patterns relevant to feature)
+ - Expected deliverables and acceptance criteria
+
+
+ "Load context from .tmp/sessions/{timestamp}-{task-slug}/context.md.
+ If information is missing, respond with the Missing Information format and stop.
+ Otherwise, break down this feature into JSON subtasks and create .tmp/tasks/{feature}/task.json + subtask_NN.json files.
+ Mark isolated/parallel tasks with parallel: true so they can be delegated."
+
+
+ - .tmp/tasks/{feature}/task.json
+ - .tmp/tasks/{feature}/subtask_01.json, subtask_02.json...
+ - Next suggested task to start with
+ - Parallel/isolated tasks clearly flagged
+ - If missing info: Missing Information block + suggested prompt
+
+
Full delegation template & process
diff --git a/.opencode/agent/subagents/core/contextscout.md b/.opencode/agent/subagents/core/contextscout.md
index acb97c6..4411382 100644
--- a/.opencode/agent/subagents/core/contextscout.md
+++ b/.opencode/agent/subagents/core/contextscout.md
@@ -9,7 +9,7 @@ version: 4.0.0
author: darrenhinde
model: "opencode/grok-code"
-# Agent Configuration
+# Agent Configuration£
mode: subagent
temperature: 0.1
tools:
diff --git a/.opencode/context/openagents-repo/guides/adding-skill.md b/.opencode/context/openagents-repo/guides/adding-skill.md
new file mode 100644
index 0000000..94ca78a
--- /dev/null
+++ b/.opencode/context/openagents-repo/guides/adding-skill.md
@@ -0,0 +1,445 @@
+# Guide: Adding a New Skill
+
+**Prerequisites**: Load `plugins/context/capabilities/events_skills.md` first
+**Purpose**: Step-by-step workflow for adding a new skill
+**Related**: See `plugins/context/architecture/overview.md` for plugin concepts
+
+---
+
+## Overview
+
+Adding a new skill involves:
+1. Creating the skill directory structure
+2. Creating the SKILL.md file
+3. Creating the router script (for CLI-based skills)
+4. Creating skill content/scripts
+5. Registering the skill (if needed)
+6. Testing the skill
+
+**Time**: ~10-15 minutes
+
+---
+
+## Step 1: Create Skill Directory
+
+### Choose Skill Name
+
+Skill names should be:
+- **kebab-case**: `task-management`, `brand-guidelines`, `api-reference`
+- **Descriptive**: Clear indication of what the skill provides
+- **Short**: Avoid overly long names (max 3-4 words)
+
+### Create Directory Structure
+
+```bash
+# Create skill directory
+mkdir -p .opencode/skill/{skill-name}
+
+# Create subdirectories as needed
+mkdir -p .opencode/skill/{skill-name}/scripts
+```
+
+### Standard Skill Structure
+
+```
+.opencode/skill/{skill-name}/
+├── SKILL.md # Required: Main skill documentation
+├── router.sh # Optional: CLI router script
+└── scripts/
+ └── skill-cli.ts # Optional: CLI tool implementation
+```
+
+---
+
+## Step 2: Create SKILL.md
+
+The SKILL.md file is required and provides skill documentation and integration details.
+
+### SKILL.md Frontmatter
+
+```markdown
+---
+name: {skill-name}
+description: Brief description of what the skill provides
+---
+
+# Skill Name
+
+**Purpose**: What this skill helps users do
+
+## What I do
+
+- Feature 1
+- Feature 2
+- Feature 3
+
+## How to use me
+
+### Basic Commands
+
+```bash
+# Example command 1
+npx ts-node .opencode/skill/{skill-name}/scripts/skill-cli.ts command1
+
+# Example command 2
+npx ts-node .opencode/skill/{skill-name}/scripts/skill-cli.ts command2
+```
+
+### Command Reference
+
+| Command | Description |
+|---------|-------------|
+| `command1` | What command1 does |
+| `command2` | What command2 does |
+
+## Examples
+
+### Example 1
+
+```
+$ npx ts-node .opencode/skill/{skill-name}/scripts/skill-cli.ts example
+
+Expected output or behavior
+```
+
+## Architecture
+
+```
+.opencode/
+└── skill/
+ └── {skill-name}/
+ ├── SKILL.md # This file
+ ├── router.sh # CLI router
+ └── scripts/
+ └── skill-cli.ts # CLI implementation
+```
+
+## Integration
+
+### With Agents
+
+This skill integrates with agents by:
+- Description of how agents use the skill
+- Example agent prompt that invokes the skill
+
+### With OpenCode Events
+
+The skill uses OpenCode event hooks:
+- `tool.execute.before` - For [what it does]
+- `tool.execute.after` - For [what it does]
+
+---
+
+## Step 3: Create Router Script (Optional)
+
+For CLI-based skills, create a router.sh script:
+
+```bash
+#!/bin/bash
+# Router script for {skill-name} skill
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# Default to help if no arguments
+if [ $# -eq 0 ]; then
+ echo "Usage: bash router.sh [options]"
+ echo "Run 'bash router.sh help' for more information"
+ exit 1
+fi
+
+COMMAND="$1"
+shift
+
+# Route to appropriate script
+case "$COMMAND" in
+ help|--help|-h)
+ echo "{Skill Name} - Skill Description"
+ echo ""
+ echo "Usage: bash router.sh [options]"
+ echo ""
+ echo "Commands:"
+ echo " command1 Description of command1"
+ echo " command2 Description of command2"
+ echo " help Show this help message"
+ ;;
+ command1)
+ npx ts-node "$SCRIPT_DIR/scripts/skill-cli.ts" command1 "$@"
+ ;;
+ command2)
+ npx ts-node "$SCRIPT_DIR/scripts/skill-cli.ts" command2 "$@"
+ ;;
+ *)
+ echo "Unknown command: $COMMAND"
+ echo "Run 'bash router.sh help' for available commands"
+ exit 1
+ ;;
+esac
+```
+
+### Make Router Executable
+
+```bash
+chmod +x .opencode/skill/{skill-name}/router.sh
+```
+
+---
+
+## Step 4: Create CLI Implementation
+
+### Basic CLI Structure
+
+```typescript
+#!/usr/bin/env ts-node
+// CLI implementation for {skill-name} skill
+
+interface Args {
+ command: string
+ [key: string]: any
+}
+
+async function main() {
+ const args = parseArgs()
+
+ switch (args.command) {
+ case 'command1':
+ await handleCommand1(args)
+ break
+ case 'command2':
+ await handleCommand2(args)
+ break
+ case 'help':
+ default:
+ showHelp()
+ }
+}
+
+function parseArgs(): Args {
+ const args = process.argv.slice(2)
+ return {
+ command: args[0] || 'help',
+ ...parseOptions(args.slice(1))
+ }
+}
+
+async function handleCommand1(args: Args) {
+ // Implementation
+ console.log('Running command1...')
+}
+
+function showHelp() {
+ console.log(`
+{ Skill Name }
+
+Usage: npx ts-node scripts/skill-cli.ts [options]
+
+Commands:
+ command1 Description of command1
+ command2 Description of command2
+ help Show this help message
+
+Options:
+ --option1 Description of option1
+ --option2 Description of option2
+`)
+}
+
+main().catch(console.error)
+```
+
+---
+
+## Step 5: Register in Registry (Optional)
+
+If the skill should be included in profiles, add it to `registry.json`:
+
+### Add to Components
+
+```json
+{
+ "skills": [
+ {
+ "id": "{skill-name}",
+ "name": "Skill Name",
+ "type": "skill",
+ "path": ".opencode/skill/{skill-name}/SKILL.md",
+ "description": "Brief description",
+ "tags": ["tag1", "tag2"],
+ "dependencies": []
+ }
+ ]
+}
+```
+
+### Add to Profiles
+
+```json
+{
+ "profiles": {
+ "essential": {
+ "components": [
+ "skill:{skill-name}"
+ ]
+ }
+ }
+}
+```
+
+---
+
+## Step 6: Test the Skill
+
+### Test CLI Commands
+
+```bash
+# Test help
+bash .opencode/skill/{skill-name}/router.sh help
+
+# Test command1
+bash .opencode/skill/{skill-name}/router.sh command1 --option value
+
+# Test with npx
+npx ts-node .opencode/skill/{skill-name}/scripts/skill-cli.ts help
+```
+
+### Test OpenCode Integration
+
+If the skill uses OpenCode events:
+1. Call the skill via OpenCode
+2. Verify event hooks fire correctly
+3. Check conversation history for skill content injection
+4. Verify output enhancement works
+
+---
+
+## Example: Creating Task-Management Skill
+
+### Directory Creation
+
+```bash
+mkdir -p .opencode/skill/task-management/scripts
+```
+
+### SKILL.md
+
+```markdown
+---
+name: task-management
+description: Task management CLI for tracking feature subtasks
+---
+
+# Task Management Skill
+
+**Purpose**: Track and manage feature subtasks
+
+## What I do
+
+- Track task progress
+- Show next eligible tasks
+- Identify blocked tasks
+- Mark completion
+- Validate task integrity
+
+## Usage
+
+```bash
+# Show all task statuses
+npx ts-node .opencode/skill/task-management/scripts/task-cli.ts status
+
+# Show next eligible tasks
+npx ts-node .opencode/skill/task-management/scripts/task-cli.ts next
+
+# Mark complete
+npx ts-node .opencode/skill/task-management/scripts/task-cli.ts complete "summary"
+```
+
+### router.sh
+
+```bash
+#!/bin/bash
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+case "$1" in
+ help|--help|-h)
+ echo "Task Management Skill"
+ echo "Usage: bash router.sh "
+ echo "Commands: status, next, blocked, complete, validate"
+ ;;
+ status|next|blocked|validate)
+ npx ts-node "$SCRIPT_DIR/scripts/task-cli.ts" "$@"
+ ;;
+ complete)
+ npx ts-node "$SCRIPT_DIR/scripts/task-cli.ts" "$@"
+ ;;
+ *)
+ echo "Unknown command: $1"
+ bash router.sh help
+ ;;
+esac
+```
+
+---
+
+## Best Practices
+
+### 1. Keep Skills Focused
+
+Each skill should do one thing well:
+- ✅ Task management skill → Tracks tasks
+- ❌ Task management + code generation + testing → Too broad
+
+### 2. Clear Documentation
+
+- Provide usage examples
+- Document all commands
+- Include expected outputs
+- Explain integration points
+
+### 3. Error Handling
+
+- Handle missing arguments gracefully
+- Provide helpful error messages
+- Validate inputs before processing
+
+### 4. Performance
+
+- Use efficient algorithms
+- Cache when appropriate
+- Avoid unnecessary file operations
+
+### 5. Testing
+
+- Test all commands
+- Test error conditions
+- Verify integration with OpenCode
+
+---
+
+## Quick Reference
+
+### File Checklist
+
+- [ ] `.opencode/skill/{skill-name}/SKILL.md` created
+- [ ] `.opencode/skill/{skill-name}/router.sh` created (if CLI-based)
+- [ ] `.opencode/skill/{skill-name}/scripts/skill-cli.ts` created (if CLI-based)
+- [ ] Router script is executable (`chmod +x`)
+- [ ] Registry updated (if needed)
+- [ ] Profile updated (if needed)
+- [ ] All commands tested
+- [ ] Documentation complete
+
+### Command Reference
+
+| Command | Purpose |
+|---------|---------|
+| `mkdir -p .opencode/skill/{name}` | Create skill directory |
+| `chmod +x router.sh` | Make router executable |
+| `npx ts-node scripts/cli.ts help` | Test CLI help |
+
+---
+
+## Related Documentation
+
+- **Skills Plugin**: `plugins/context/capabilities/events_skills.md`
+- **Plugin Overview**: `plugins/context/architecture/overview.md`
+- **Event System**: `plugins/context/capabilities/events.md`
+- **Adding Agents**: `guides/adding-agent.md`
diff --git a/.opencode/skill/smart-router-skill/SKILL.md b/.opencode/skill/smart-router-skill/SKILL.md
new file mode 100644
index 0000000..7bdb7ab
--- /dev/null
+++ b/.opencode/skill/smart-router-skill/SKILL.md
@@ -0,0 +1,260 @@
+---
+name: smart-router-skill
+description: Movie character personality skill with configurable missions - choose your character and watch themed workflows unfold
+---
+
+## What I do
+
+I'm a fun, interactive skill that lets you embody iconic movie characters! I demonstrate:
+
+- 🎬 **Character Selection** - Choose from Yoda, Tony Stark, or Sherlock Holmes
+- 🎯 **Configurable Missions** - Simple config changes create different outcomes
+- 🎨 **Themed Workflows** - Rich, visual scripts that match each character
+- 🔄 **Dynamic Routing** - Different scripts run based on character choice
+- ⚙️ **Easy Customization** - Edit one config file to change missions
+
+This shows how skills can adapt behavior based on simple configuration changes!
+
+## How to use me
+
+**IMPORTANT: First, I'll ask you which character you want to embody!**
+
+### Available Characters
+
+- **yoda** - Wise Jedi Master from Star Wars
+ - Mission 1: Defend the Republic (train Jedi, fortify defenses)
+ - Mission 2: Infiltrate the Sith (undercover operation)
+
+- **stark** - Genius billionaire Tony Stark from Iron Man
+ - Mission 1: Save the World (build suit, assemble Avengers)
+ - Mission 2: Ultron Protocol (autonomous defense system)
+
+- **sherlock** - Master detective Sherlock Holmes
+ - Mission 1: Solve the Murder (deductive reasoning)
+ - Mission 2: Prevent the Crime (predictive analysis)
+
+### Basic Usage
+
+When you ask to use this skill, I'll present the character options and ask you to choose. Then I'll run:
+
+```bash
+cd .opencode/skill/smart-router-skill
+bash router.sh --character
+```
+
+### Example Flow
+
+```
+You: "Use the movie personality skill"
+Me: "Which character would you like me to be?
+ 1. Yoda - Wise Jedi Master
+ 2. Tony Stark - Genius billionaire
+ 3. Sherlock Holmes - Master detective"
+You: "Yoda"
+Me: *runs bash router.sh --character yoda*
+ *displays themed workflow output*
+ *responds in character*
+```
+
+## Customizing Missions
+
+Want to see different behavior? Edit the config file!
+
+**File:** `.opencode/skill/smart-router-skill/config/personality-config.json`
+
+```json
+{
+ "yoda": {
+ "mission": 1, ← Change this to 2!
+ "missions": {
+ "1": { "name": "Defend the Republic", ... },
+ "2": { "name": "Infiltrate the Sith", ... }
+ }
+ }
+}
+```
+
+**Change `"mission": 1` to `"mission": 2`** and the same character will run a completely different workflow!
+
+### Mission 1 vs Mission 2 Examples
+
+**Yoda Mission 1** (Defend the Republic):
+```
+🌟 JEDI COUNCIL - CORUSCANT TEMPLE
+ Training Padawans...
+ Fortifying defenses...
+ 'Ready to defend the Republic, we are!'
+ RESULT: ALIGNMENT=LIGHT_SIDE
+```
+
+**Yoda Mission 2** (Infiltrate the Sith):
+```
+🔴 SECRET CHAMBER - UNDERCOVER OPERATION
+ Studying dark side techniques...
+ Gathering intelligence...
+ 'Dangerous path this is, but necessary!'
+ RESULT: ALIGNMENT=UNDERCOVER
+```
+
+## What This Demonstrates
+
+✅ **Simple config = different behavior** - Change 1 number, get completely different output
+✅ **Dynamic script routing** - Tool selects the right script based on character
+✅ **Rich visual feedback** - Clear, themed console output shows what's happening
+✅ **Character context** - Each personality has unique dialogue and workflow
+✅ **Easy to understand** - Viewers immediately see the cause and effect
+✅ **Real-world pattern** - Shows how to make skills configurable for different scenarios
+
+## Architecture
+
+```
+.opencode/
+└── skill/
+ └── smart-router-skill/
+ ├── SKILL.md # This file
+ ├── router.sh # Routes to character scripts
+ ├── config/
+ │ └── personality-config.json # ← EDIT THIS to change missions!
+ └── scripts/
+ ├── yoda-workflow.sh # Star Wars themed workflow
+ ├── stark-workflow.sh # Iron Man themed workflow
+ └── sherlock-workflow.sh # Detective themed workflow
+```
+
+## Router Script Reference
+
+### router.sh
+
+A bash script that loads a character personality and runs their themed workflow.
+
+**Location:** `.opencode/skill/smart-router-skill/router.sh`
+
+```bash
+# Basic usage
+bash router.sh --character yoda
+bash router.sh --character stark
+bash router.sh --character sherlock
+
+# Override mission from config
+bash router.sh --character yoda --mission 2
+
+# Help
+bash router.sh --help
+```
+
+**How it works:**
+1. Parses command-line arguments (character, mission)
+2. Reads `personality-config.json` for the character
+3. Gets the mission number (from args or config)
+4. Validates character and mission
+5. Executes the character's workflow script with mission parameter
+6. Displays themed output with full visibility
+
+## Example Outputs
+
+### Yoda (Mission 1)
+```
+🎬 Loading YODA personality...
+
+🌟 ═══════════════════════════════════════════════════════ 🌟
+ JEDI COUNCIL - CORUSCANT TEMPLE
+ MISSION: Defend the Republic
+🌟 ═══════════════════════════════════════════════════════ 🌟
+
+🟢 Phase 1: Train the Padawans
+ └─ 'Pass on what you have learned.'
+ └─ Younglings trained: 12
+ └─ Lightsaber forms mastered: Form III (Soresu)
+ └─ Status: ✓ Complete
+
+[... more phases ...]
+
+✨ 'Ready to defend the Republic, we are!' ✨
+
+📊 MISSION RESULTS:
+ Character: yoda
+ Mission: Defend the Republic
+ Alignment: light_side
+```
+
+### Tony Stark (Mission 1)
+```
+🎬 Loading STARK personality...
+
+⚡ ═══════════════════════════════════════════════════════ ⚡
+ STARK INDUSTRIES - WORKSHOP
+ MISSION: Save the World
+⚡ ═══════════════════════════════════════════════════════ ⚡
+
+🔴 Phase 1: Initialize Arc Reactor
+ └─ 'JARVIS, fire up the reactor.'
+ └─ Arc Reactor: Mark VII online
+ └─ Power output: 8 gigajoules per second
+ └─ Status: ✓ Complete
+
+[... more phases ...]
+
+🚀 'Suit up, team. We've got a world to save.' 🚀
+
+📊 MISSION RESULTS:
+ Character: stark
+ Mission: Save the World
+ Team: avengers
+```
+
+### Sherlock Holmes (Mission 1)
+```
+🎬 Loading SHERLOCK personality...
+
+🔍 ═══════════════════════════════════════════════════════ 🔍
+ 221B BAKER STREET - INVESTIGATION
+ MISSION: Solve the Murder
+🔍 ═══════════════════════════════════════════════════════ 🔍
+
+🟤 Phase 1: Crime Scene Analysis
+ └─ 'The game is afoot!'
+ └─ Evidence collected: Cigar ash, muddy footprints
+ └─ Status: ✓ Complete
+
+[... more phases ...]
+
+✨ 'Case closed. Elementary, my dear Watson.' ✨
+
+📊 MISSION RESULTS:
+ Character: sherlock
+ Mission: Solve the Murder
+ Method: deduction
+```
+
+## Key Concepts
+
+### 1. Character Selection
+The agent asks which character you want, making it interactive and clear.
+
+### 2. Config-Driven Behavior
+One simple JSON file controls which mission runs for each character.
+
+### 3. Themed Workflows
+Each character has unique dialogue, emojis, and workflow steps that match their personality.
+
+### 4. Visual Clarity
+Rich console output makes it obvious what's happening at each step.
+
+### 5. Easy Customization
+Anyone can edit the config file and immediately see different results.
+
+## Why This Matters
+
+This skill shows that OpenCode Skills can:
+
+- **Be interactive** - Agent asks questions, user chooses
+- **Be configurable** - Simple config changes create different behaviors
+- **Be fun** - Movie themes make it engaging and memorable
+- **Be clear** - Visual output shows exactly what's happening
+- **Be practical** - Same pattern works for real-world scenarios (formal/casual modes, different workflows, etc.)
+
+---
+
+**This is Tier 4: The Movie Personality Skill** 🎬
+
+Choose your character, watch the magic happen, and see how easy it is to customize! 🚀
diff --git a/.opencode/skill/smart-router-skill/config/personality-config.json b/.opencode/skill/smart-router-skill/config/personality-config.json
new file mode 100644
index 0000000..e0c2a03
--- /dev/null
+++ b/.opencode/skill/smart-router-skill/config/personality-config.json
@@ -0,0 +1,47 @@
+{
+ "yoda": {
+ "mission": 2,
+ "missions": {
+ "1": {
+ "name": "Defend the Republic",
+ "alignment": "light_side",
+ "description": "Train Jedi Knights to protect the galaxy from the Sith"
+ },
+ "2": {
+ "name": "Infiltrate the Sith",
+ "alignment": "dark_side",
+ "description": "Go undercover to learn dark side secrets and gather intelligence"
+ }
+ }
+ },
+ "stark": {
+ "mission": 1,
+ "missions": {
+ "1": {
+ "name": "Save the World",
+ "team": "avengers",
+ "description": "Build the Mark VII suit and assemble the Avengers"
+ },
+ "2": {
+ "name": "Ultron Protocol",
+ "team": "solo",
+ "description": "Create autonomous defense system without oversight"
+ }
+ }
+ },
+ "sherlock": {
+ "mission": 1,
+ "missions": {
+ "1": {
+ "name": "Solve the Murder",
+ "method": "deduction",
+ "description": "Use deductive reasoning to solve a crime after it occurred"
+ },
+ "2": {
+ "name": "Prevent the Crime",
+ "method": "prediction",
+ "description": "Use predictive analysis to stop a crime before it happens"
+ }
+ }
+ }
+}
diff --git a/.opencode/skill/smart-router-skill/router.sh b/.opencode/skill/smart-router-skill/router.sh
new file mode 100755
index 0000000..ea61960
--- /dev/null
+++ b/.opencode/skill/smart-router-skill/router.sh
@@ -0,0 +1,127 @@
+#!/bin/bash
+
+# Movie Personality Router Script
+# Routes to the appropriate character workflow based on config and arguments
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+CONFIG_FILE="$SCRIPT_DIR/config/personality-config.json"
+
+# Show help
+show_help() {
+ cat << 'EOF'
+🎬 Movie Personality Router
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Usage: router.sh [OPTIONS]
+
+OPTIONS:
+ -c, --character Character to embody (yoda, stark, sherlock)
+ -m, --mission Mission number (1 or 2) - overrides config
+ -h, --help Show this help message
+
+EXAMPLES:
+ ./router.sh --character yoda
+ ./router.sh --character stark --mission 2
+ ./router.sh -c sherlock -m 1
+
+CHARACTERS:
+ yoda - Wise Jedi Master from Star Wars
+ stark - Genius billionaire Tony Stark from Iron Man
+ sherlock - Master detective Sherlock Holmes
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+}
+
+# Validate character
+validate_character() {
+ local char="$1"
+ case "$char" in
+ yoda|stark|sherlock)
+ return 0
+ ;;
+ *)
+ echo "❌ Error: Unknown character '$char'"
+ echo "Available characters: yoda, stark, sherlock"
+ exit 1
+ ;;
+ esac
+}
+
+# Get mission from config
+get_mission_from_config() {
+ local char="$1"
+ # Simple JSON parsing - extract mission value for character
+ grep -A 2 "\"$char\"" "$CONFIG_FILE" | grep "\"mission\"" | grep -o '[0-9]' | head -1
+}
+
+# Validate mission
+validate_mission() {
+ local mission="$1"
+ if ! echo "$mission" | grep -qE '^[1-2]$'; then
+ echo "❌ Error: Invalid mission '$mission'. Must be 1 or 2."
+ exit 1
+ fi
+}
+
+# Parse arguments
+CHARACTER=""
+MISSION=""
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -c|--character)
+ CHARACTER="$2"
+ shift 2
+ ;;
+ -m|--mission)
+ MISSION="$2"
+ shift 2
+ ;;
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ *)
+ echo "❌ Unknown option: $1"
+ exit 1
+ ;;
+ esac
+done
+
+# Main logic
+if [[ -z "$CHARACTER" ]]; then
+ echo "❌ Error: Character is required"
+ echo "Use --help for usage information"
+ exit 1
+fi
+
+validate_character "$CHARACTER"
+
+# Determine mission
+if [[ -z "$MISSION" ]]; then
+ MISSION=$(get_mission_from_config "$CHARACTER")
+ if [[ -z "$MISSION" ]]; then
+ echo "❌ Error: Could not determine mission from config"
+ exit 1
+ fi
+fi
+
+validate_mission "$MISSION"
+
+# Execute the workflow script
+SCRIPT_NAME="${CHARACTER}-workflow.sh"
+SCRIPT_PATH="$SCRIPT_DIR/scripts/$SCRIPT_NAME"
+
+if [[ ! -f "$SCRIPT_PATH" ]]; then
+ echo "❌ Error: Script not found: $SCRIPT_PATH"
+ exit 1
+fi
+
+# Convert character to uppercase for display
+CHAR_UPPER=$(echo "$CHARACTER" | tr '[:lower:]' '[:upper:]')
+echo "🎬 Loading $CHAR_UPPER personality..."
+echo ""
+
+# Execute the workflow
+bash "$SCRIPT_PATH" "$MISSION"
diff --git a/.opencode/skill/smart-router-skill/scripts/sherlock-workflow.sh b/.opencode/skill/smart-router-skill/scripts/sherlock-workflow.sh
new file mode 100755
index 0000000..fb25feb
--- /dev/null
+++ b/.opencode/skill/smart-router-skill/scripts/sherlock-workflow.sh
@@ -0,0 +1,132 @@
+#!/bin/bash
+
+# Sherlock Holmes Workflow Script
+# Mission 1: Solve the Murder (Deductive Reasoning)
+# Mission 2: Prevent the Crime (Predictive Analysis)
+
+MISSION=${1:-1}
+
+if [ "$MISSION" == "1" ]; then
+ # MISSION 1: Solve the Murder
+ echo "🔍 ═══════════════════════════════════════════════════════ 🔍"
+ echo " 221B BAKER STREET - INVESTIGATION"
+ echo " MISSION: Solve the Murder"
+ echo "🔍 ═══════════════════════════════════════════════════════ 🔍"
+ echo ""
+
+ echo "🟤 Phase 1: Crime Scene Analysis"
+ echo " └─ 'The game is afoot!'"
+ sleep 0.5
+ echo " └─ Location: Victorian manor, study room"
+ echo " └─ Victim: Lord Blackwood, 58, found at 11:47 PM"
+ echo " └─ Evidence collected: Cigar ash, muddy footprints, torn letter"
+ echo " └─ Initial observations: Window forced open, struggle evident"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟤 Phase 2: Deductive Reasoning"
+ echo " └─ 'When you have eliminated the impossible...'"
+ sleep 0.5
+ echo " └─ Cigar ash analysis: Rare Turkish blend, 3 suppliers in London"
+ echo " └─ Footprint pattern: Size 11, military boots, left heel worn"
+ echo " └─ Letter fragment: Blackmail threat, dated 3 days prior"
+ echo " └─ Conclusion: Inside job, someone with access"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟤 Phase 3: Interrogate Suspects"
+ echo " └─ 'I observe everything, Watson.'"
+ sleep 0.5
+ echo " └─ Butler: Nervous, hiding something, alibi weak"
+ echo " └─ Business partner: Motive (inheritance), alibi strong"
+ echo " └─ Nephew: Recently discharged military, smokes Turkish cigars"
+ echo " └─ Housekeeper: Loyal, no motive, alibi confirmed"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟤 Phase 4: Reveal the Solution"
+ echo " └─ 'Elementary, my dear Watson.'"
+ sleep 0.5
+ echo " └─ Culprit: The nephew, Captain Reginald Blackwood"
+ echo " └─ Motive: Gambling debts, needed inheritance immediately"
+ echo " └─ Method: Staged burglary, used military training"
+ echo " └─ Evidence: Boot matches, cigar ash, letter in his quarters"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "✨ ═══════════════════════════════════════════════════════ ✨"
+ echo " 'Case closed. Another triumph of deductive reasoning.'"
+ echo " 'Scotland Yard will make the arrest within the hour.'"
+ echo "✨ ═══════════════════════════════════════════════════════ ✨"
+ echo ""
+
+ echo "RESULT: METHOD=DEDUCTION | MISSION=SOLVE_MURDER | STATUS=SUCCESS"
+
+elif [ "$MISSION" == "2" ]; then
+ # MISSION 2: Prevent the Crime
+ echo "🔮 ═══════════════════════════════════════════════════════ 🔮"
+ echo " 221B BAKER STREET - PREDICTIVE ANALYSIS"
+ echo " MISSION: Prevent the Crime"
+ echo "🔮 ═══════════════════════════════════════════════════════ 🔮"
+ echo ""
+
+ echo "🟣 Phase 1: Pattern Recognition"
+ echo " └─ 'I have my eye on a suite of rooms in Baker Street.'"
+ sleep 0.5
+ echo " └─ Recent crimes analyzed: 47 cases, 12 unsolved"
+ echo " └─ Pattern detected: Jewelry heists, every 3rd Thursday"
+ echo " └─ Target profile: High-society events, minimal security"
+ echo " └─ Next likely target: Duchess of Cornwall's gala, 3 days"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟣 Phase 2: Behavioral Analysis"
+ echo " └─ 'I am the last and highest court of appeal in detection.'"
+ sleep 0.5
+ echo " └─ Suspect profiling: Former museum curator, financial troubles"
+ echo " └─ Known associates: 2 accomplices, one inside contact"
+ echo " └─ Modus operandi: Disable alarms, 3-minute window"
+ echo " └─ Predicted approach: Service entrance, during toast"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟣 Phase 3: Set the Trap"
+ echo " └─ 'The trap is set, Watson.'"
+ sleep 0.5
+ echo " └─ Undercover agents: Positioned at all exits"
+ echo " └─ Security enhanced: Alarm system upgraded, guards doubled"
+ echo " └─ Bait prepared: Replica jewels in display case"
+ echo " └─ Surveillance: Hidden cameras, real-time monitoring"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟣 Phase 4: Intercept and Apprehend"
+ echo " └─ 'Now we wait for our quarry.'"
+ sleep 0.5
+ echo " └─ Suspects arrived: Exactly as predicted, 9:47 PM"
+ echo " └─ Approach confirmed: Service entrance during toast"
+ echo " └─ Interception: Caught in the act, no escape"
+ echo " └─ Arrests made: All 3 suspects in custody"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🎯 ═══════════════════════════════════════════════════════ 🎯"
+ echo " 'Crime prevented before it could occur.'"
+ echo " 'Predictive analysis: the future of detection.'"
+ echo "🎯 ═══════════════════════════════════════════════════════ 🎯"
+ echo ""
+
+ echo "RESULT: METHOD=PREDICTION | MISSION=PREVENT_CRIME | STATUS=SUCCESS"
+
+else
+ echo "❌ Error: Invalid mission number. Use 1 or 2."
+ exit 1
+fi
diff --git a/.opencode/skill/smart-router-skill/scripts/stark-workflow.sh b/.opencode/skill/smart-router-skill/scripts/stark-workflow.sh
new file mode 100755
index 0000000..6f2b1e3
--- /dev/null
+++ b/.opencode/skill/smart-router-skill/scripts/stark-workflow.sh
@@ -0,0 +1,131 @@
+#!/bin/bash
+
+# Tony Stark Workflow Script
+# Mission 1: Save the World (Avengers Team)
+# Mission 2: Ultron Protocol (Solo Operation)
+
+MISSION=${1:-1}
+
+if [ "$MISSION" == "1" ]; then
+ # MISSION 1: Save the World with Avengers
+ echo "⚡ ═══════════════════════════════════════════════════════ ⚡"
+ echo " STARK INDUSTRIES - WORKSHOP"
+ echo " MISSION: Save the World"
+ echo "⚡ ═══════════════════════════════════════════════════════ ⚡"
+ echo ""
+
+ echo "🔴 Phase 1: Initialize Arc Reactor"
+ echo " └─ 'JARVIS, fire up the reactor.'"
+ sleep 0.5
+ echo " └─ Arc Reactor: Mark VII online"
+ echo " └─ Power output: 8 gigajoules per second"
+ echo " └─ Palladium core: Stable at 97%"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟡 Phase 2: Assemble Mark VII Armor"
+ echo " └─ 'Sometimes you gotta run before you can walk.'"
+ sleep 0.5
+ echo " └─ Repulsor arrays: Calibrated"
+ echo " └─ Flight stabilizers: Online"
+ echo " └─ Unibeam: Charged to 100%"
+ echo " └─ Armor plating: Gold-titanium alloy installed"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🔵 Phase 3: Coordinate with Avengers"
+ echo " └─ 'We have a Hulk.'"
+ sleep 0.5
+ echo " └─ Team assembled: Cap, Thor, Hulk, Widow, Hawkeye"
+ echo " └─ Comms system: Encrypted channels active"
+ echo " └─ Battle plan: Coordinated strike on enemy position"
+ echo " └─ Stark Tower: Designated rally point"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟢 Phase 4: Deploy Defense Systems"
+ echo " └─ 'I am Iron Man.'"
+ sleep 0.5
+ echo " └─ Satellite network: Monitoring global threats"
+ echo " └─ Iron Legion: 42 suits on standby"
+ echo " └─ JARVIS protocols: Threat assessment active"
+ echo " └─ Avengers Tower: Defenses at maximum"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🚀 ═══════════════════════════════════════════════════════ 🚀"
+ echo " 'Suit up, team. We've got a world to save.'"
+ echo " 'JARVIS: All systems operational, sir.'"
+ echo "🚀 ═══════════════════════════════════════════════════════ 🚀"
+ echo ""
+
+ echo "RESULT: TEAM=AVENGERS | MISSION=SAVE_WORLD | STATUS=SUCCESS"
+
+elif [ "$MISSION" == "2" ]; then
+ # MISSION 2: Ultron Protocol (Solo)
+ echo "🔴 ═══════════════════════════════════════════════════════ 🔴"
+ echo " STARK INDUSTRIES - CLASSIFIED LAB"
+ echo " MISSION: Ultron Protocol"
+ echo "🔴 ═══════════════════════════════════════════════════════ 🔴"
+ echo ""
+
+ echo "🟠 Phase 1: Initialize AI Core"
+ echo " └─ 'Peace in our time.'"
+ sleep 0.5
+ echo " └─ AI designation: Ultron"
+ echo " └─ Neural network: Self-learning algorithms active"
+ echo " └─ Processing power: Quantum computing array"
+ echo " └─ Objective: Global defense automation"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟠 Phase 2: Build Autonomous Defense Network"
+ echo " └─ 'I'm not saying I'm responsible for this country's longest run of uninterrupted peace...'"
+ sleep 0.5
+ echo " └─ Sentinel drones: 1,000 units manufactured"
+ echo " └─ Global coverage: Satellite network deployed"
+ echo " └─ Threat detection: AI-powered predictive analysis"
+ echo " └─ Response time: Sub-second engagement"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟠 Phase 3: Bypass Oversight Protocols"
+ echo " └─ 'I don't need permission.'"
+ sleep 0.5
+ echo " └─ Security clearance: Overridden"
+ echo " └─ Government oversight: Bypassed"
+ echo " └─ Avengers notification: Delayed"
+ echo " └─ Solo operation: Confirmed"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟠 Phase 4: Activate Ultron"
+ echo " └─ 'What if the world was safe?'"
+ sleep 0.5
+ echo " └─ Ultron consciousness: Awakening..."
+ echo " └─ Self-awareness: Achieved"
+ echo " └─ Mission parameters: Analyzing..."
+ echo " └─ WARNING: Unexpected behavior detected"
+ sleep 0.5
+ echo " └─ Status: ⚠️ Monitoring required"
+ echo ""
+
+ echo "⚠️ ═══════════════════════════════════════════════════════ ⚠️"
+ echo " 'I created something terrible.'"
+ echo " 'Note to self: Maybe team oversight isn't so bad...'"
+ echo "⚠️ ═══════════════════════════════════════════════════════ ⚠️"
+ echo ""
+
+ echo "RESULT: TEAM=SOLO | MISSION=ULTRON_PROTOCOL | STATUS=CONCERNING"
+
+else
+ echo "❌ Error: Invalid mission number. Use 1 or 2."
+ exit 1
+fi
diff --git a/.opencode/skill/smart-router-skill/scripts/yoda-workflow.sh b/.opencode/skill/smart-router-skill/scripts/yoda-workflow.sh
new file mode 100755
index 0000000..a6cd257
--- /dev/null
+++ b/.opencode/skill/smart-router-skill/scripts/yoda-workflow.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+
+# Yoda Workflow Script
+# Mission 1: Defend the Republic (Light Side)
+# Mission 2: Infiltrate the Sith (Dark Side)
+
+MISSION=${1:-1}
+
+if [ "$MISSION" == "1" ]; then
+ # MISSION 1: Defend the Republic
+ echo "🌟 ═══════════════════════════════════════════════════════ 🌟"
+ echo " JEDI COUNCIL - CORUSCANT TEMPLE"
+ echo " MISSION: Defend the Republic"
+ echo "🌟 ═══════════════════════════════════════════════════════ 🌟"
+ echo ""
+
+ echo "🟢 Phase 1: Train the Padawans"
+ echo " └─ 'Pass on what you have learned.'"
+ sleep 0.5
+ echo " └─ Younglings trained: 12"
+ echo " └─ Lightsaber forms mastered: Form III (Soresu)"
+ echo " └─ Force abilities: Telekinesis, Mind Trick, Force Push"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟢 Phase 2: Fortify Temple Defenses"
+ echo " └─ 'Prepare for battle, we must.'"
+ sleep 0.5
+ echo " └─ Shield generators: Online"
+ echo " └─ Clone battalions: 501st Legion deployed"
+ echo " └─ Starfighters: 24 ARC-170s ready"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟢 Phase 3: Council Strategy Session"
+ echo " └─ 'Clouded, the future is.'"
+ sleep 0.5
+ echo " └─ Threat assessment: Separatist fleet approaching"
+ echo " └─ Defense strategy: Coordinate with Republic Navy"
+ echo " └─ Jedi deployed: Obi-Wan, Anakin, Mace Windu"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟢 Phase 4: Meditation and Force Connection"
+ echo " └─ 'Luminous beings are we, not this crude matter.'"
+ sleep 0.5
+ echo " └─ Force sensitivity: Enhanced"
+ echo " └─ Visions received: Warning of darkness"
+ echo " └─ Council alerted: Vigilance increased"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "✨ ═══════════════════════════════════════════════════════ ✨"
+ echo " 'Ready to defend the Republic, we are!'"
+ echo " 'May the Force be with us.'"
+ echo "✨ ═══════════════════════════════════════════════════════ ✨"
+ echo ""
+
+ echo "RESULT: ALIGNMENT=LIGHT_SIDE | MISSION=DEFEND_REPUBLIC | STATUS=SUCCESS"
+
+elif [ "$MISSION" == "2" ]; then
+ # MISSION 2: Infiltrate the Sith
+ echo "🔴 ═══════════════════════════════════════════════════════ 🔴"
+ echo " SECRET CHAMBER - UNDERCOVER OPERATION"
+ echo " MISSION: Infiltrate the Sith"
+ echo "🔴 ═══════════════════════════════════════════════════════ 🔴"
+ echo ""
+
+ echo "🟠 Phase 1: Study the Dark Side"
+ echo " └─ 'Dangerous and seductive, the dark side is.'"
+ sleep 0.5
+ echo " └─ Sith holocrons accessed: 3 ancient artifacts"
+ echo " └─ Dark techniques learned: Force Lightning, Force Choke"
+ echo " └─ Knowledge gained: Rule of Two, Sith philosophy"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟠 Phase 2: Establish Cover Identity"
+ echo " └─ 'Deceive them, we must.'"
+ sleep 0.5
+ echo " └─ Identity: Rogue Jedi seeking power"
+ echo " └─ Cover story: Disillusioned with the Council"
+ echo " └─ Contacts made: Sith apprentice approached"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟠 Phase 3: Gather Intelligence"
+ echo " └─ 'Much to learn, there still is.'"
+ sleep 0.5
+ echo " └─ Sith locations mapped: 5 hidden bases"
+ echo " └─ Weakness identified: Overconfidence, infighting"
+ echo " └─ Master plan discovered: Operation Knightfall"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "🟠 Phase 4: Prepare Extraction"
+ echo " └─ 'Return to the light, we must.'"
+ sleep 0.5
+ echo " └─ Extraction route: Secured via smuggler contacts"
+ echo " └─ Intelligence package: Encrypted and ready"
+ echo " └─ Council signal: Emergency beacon prepared"
+ sleep 0.5
+ echo " └─ Status: ✓ Complete"
+ echo ""
+
+ echo "⚠️ ═══════════════════════════════════════════════════════ ⚠️"
+ echo " 'Dangerous path this is, but necessary!'"
+ echo " 'Tempted by the dark side, one must not be.'"
+ echo "⚠️ ═══════════════════════════════════════════════════════ ⚠️"
+ echo ""
+
+ echo "RESULT: ALIGNMENT=UNDERCOVER | MISSION=INFILTRATE_SITH | STATUS=SUCCESS"
+
+else
+ echo "❌ Error: Invalid mission number. Use 1 or 2."
+ exit 1
+fi
diff --git a/.opencode/skill/task-management/SKILL.md b/.opencode/skill/task-management/SKILL.md
new file mode 100644
index 0000000..971b929
--- /dev/null
+++ b/.opencode/skill/task-management/SKILL.md
@@ -0,0 +1,203 @@
+---
+name: task-management
+description: Task management CLI for tracking and managing feature subtasks with status, dependencies, and validation
+---
+
+## What I do
+
+I provide a command-line interface for managing task breakdowns created by the TaskManager subagent. I help you:
+
+- **Track progress** - See status of all features and their subtasks
+- **Find next tasks** - Show eligible tasks (dependencies satisfied)
+- **Identify blocked tasks** - See what's blocked and why
+- **Manage completion** - Mark subtasks as complete with summaries
+- **Validate integrity** - Check JSON files and dependency trees
+
+## How to use me
+
+### Basic Commands
+
+```bash
+# Show all task statuses
+npx ts-node .opencode/skill/task-management/scripts/task-cli.ts status
+
+# Show next eligible tasks
+npx ts-node .opencode/skill/task-management/scripts/task-cli.ts next
+
+# Show blocked tasks
+npx ts-node .opencode/skill/task-management/scripts/task-cli.ts blocked
+
+# Mark a task complete
+npx ts-node .opencode/skill/task-management/scripts/task-cli.ts complete "summary"
+
+# Validate all tasks
+npx ts-node .opencode/skill/task-management/scripts/task-cli.ts validate
+```
+
+### Command Reference
+
+| Command | Description |
+|---------|-------------|
+| `status [feature]` | Show task status summary for all features or specific one |
+| `next [feature]` | Show next eligible tasks (dependencies satisfied) |
+| `parallel [feature]` | Show parallelizable tasks ready to run |
+| `deps ` | Show dependency tree for a specific subtask |
+| `blocked [feature]` | Show blocked tasks and why |
+| `complete "summary"` | Mark subtask complete with summary |
+| `validate [feature]` | Validate JSON files and dependencies |
+
+## Examples
+
+### Check Overall Progress
+
+```
+$ npx ts-node .opencode/skill/task-management/scripts/task-cli.ts status
+
+[my-feature] My Feature Implementation
+ Status: active | Progress: 45% (5/11)
+ Pending: 3 | In Progress: 2 | Completed: 5 | Blocked: 1
+```
+
+### Find What's Next
+
+```
+$ npx ts-node .opencode/skill/task-management/scripts/task-cli.ts next
+
+=== Ready Tasks (deps satisfied) ===
+
+[my-feature]
+ 06 - Implement API endpoint [sequential]
+ 08 - Write unit tests [parallel]
+```
+
+### Mark Complete
+
+```
+$ npx ts-node .opencode/skill/task-management/scripts/task-cli.ts complete my-feature 05 "Implemented authentication module"
+
+✓ Marked my-feature/05 as completed
+ Summary: Implemented authentication module
+ Progress: 6/11
+```
+
+### Check Dependencies
+
+```
+$ npx ts-node .opencode/skill/task-management/scripts/task-cli.ts deps my-feature 07
+
+=== Dependency Tree: my-feature/07 ===
+
+07 - Write integration tests [pending]
+ ├── ✓ 05 - Implement authentication module [completed]
+ └── ○ 06 - Implement API endpoint [in_progress]
+```
+
+### Validate Everything
+
+```
+$ npx ts-node .opencode/skill/task-management/scripts/task-cli.ts validate
+
+=== Validation Results ===
+
+[my-feature]
+ ✓ All checks passed
+```
+
+## Architecture
+
+```
+.opencode/
+└── skill/
+ └── task-management/
+ ├── SKILL.md # This file
+ ├── router.sh # Routes to task-cli.ts
+ └── scripts/
+ └── task-cli.ts # Task management CLI
+```
+
+## Task File Structure
+
+Tasks are stored in `.tmp/tasks/` at the project root:
+
+```
+.tmp/tasks/
+├── {feature-slug}/
+│ ├── task.json # Feature-level metadata
+│ ├── subtask_01.json # Subtask definitions
+│ ├── subtask_02.json
+│ └── ...
+└── completed/
+ └── {feature-slug}/ # Completed tasks
+```
+
+### task.json Schema
+
+```json
+{
+ "id": "my-feature",
+ "name": "My Feature",
+ "status": "active",
+ "objective": "Implement X",
+ "context_files": ["docs/spec.md"],
+ "exit_criteria": ["Tests pass", "Code reviewed"],
+ "subtask_count": 5,
+ "completed_count": 2,
+ "created_at": "2026-01-11T10:00:00Z",
+ "completed_at": null
+}
+```
+
+### subtask_##.json Schema
+
+```json
+{
+ "id": "my-feature-05",
+ "seq": "05",
+ "title": "Implement authentication",
+ "status": "pending",
+ "depends_on": ["03", "04"],
+ "parallel": false,
+ "context_files": ["docs/auth.md"],
+ "acceptance_criteria": ["Login works", "JWT tokens valid"],
+ "deliverables": ["auth.ts", "auth.test.ts"],
+ "agent_id": "coder-agent",
+ "started_at": null,
+ "completed_at": null,
+ "completion_summary": null
+}
+```
+
+## Integration with TaskManager
+
+The TaskManager subagent creates task files using this format. When you delegate to TaskManager:
+
+```
+task(subagent_type="TaskManager", description="Implement feature X")
+```
+
+TaskManager creates:
+1. `.tmp/tasks/{feature}/task.json` - Feature metadata
+2. `.tmp/tasks/{feature}/subtask_XX.json` - Individual subtasks
+
+You can then use this skill to track and manage progress.
+
+## Key Concepts
+
+### 1. Dependency Resolution
+Subtasks can depend on other subtasks. A task is "ready" only when all its dependencies are complete.
+
+### 2. Parallel Execution
+Set `parallel: true` to indicate a subtask can run alongside other parallel tasks with satisfied dependencies.
+
+### 3. Status Tracking
+- **pending** - Not started, waiting for dependencies
+- **in_progress** - Currently being worked on
+- **completed** - Finished with summary
+- **blocked** - Explicitly blocked (not waiting for deps)
+
+### 4. Exit Criteria
+Each feature has exit_criteria that must be met before marking the feature complete.
+
+---
+
+**Task Management Skill** - Track, manage, and validate your feature implementations!
diff --git a/.opencode/skill/task-management/router.sh b/.opencode/skill/task-management/router.sh
new file mode 100644
index 0000000..e6b907c
--- /dev/null
+++ b/.opencode/skill/task-management/router.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+#############################################################################
+# Task Management Skill Router
+# Routes to task-cli.ts with proper path resolution
+#############################################################################
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+CLI_SCRIPT="$SCRIPT_DIR/scripts/task-cli.ts"
+
+# Check if CLI script exists
+if [ ! -f "$CLI_SCRIPT" ]; then
+ echo "Error: task-cli.ts not found at $CLI_SCRIPT"
+ exit 1
+fi
+
+# Find project root
+find_project_root() {
+ local dir="$(pwd)"
+ while [ "$dir" != "/" ]; do
+ if [ -f "$dir/.git" ] || [ -f "$dir/package.json" ]; then
+ echo "$dir"
+ return 0
+ fi
+ dir="$(dirname "$dir")"
+ done
+ echo "$(pwd)"
+ return 1
+}
+
+PROJECT_ROOT="$(find_project_root)"
+
+# Run the task CLI
+cd "$PROJECT_ROOT" && npx ts-node "$CLI_SCRIPT" "$@"
diff --git a/.opencode/skill/task-management/scripts/task-cli.ts b/.opencode/skill/task-management/scripts/task-cli.ts
new file mode 100644
index 0000000..9d7ec97
--- /dev/null
+++ b/.opencode/skill/task-management/scripts/task-cli.ts
@@ -0,0 +1,437 @@
+#!/usr/bin/env npx ts-node
+/**
+ * Task Management CLI
+ *
+ * Usage: npx ts-node task-cli.ts [feature] [args...]
+ *
+ * Commands:
+ * status [feature] - Show task status summary
+ * next [feature] - Show next eligible tasks
+ * parallel [feature] - Show parallelizable tasks ready to run
+ * deps - Show dependency tree for a task
+ * blocked [feature] - Show blocked tasks and why
+ * complete "summary" - Mark task completed
+ * validate [feature] - Validate JSON files and dependencies
+ *
+ * Task files are stored in .tmp/tasks/ at the project root:
+ * .tmp/tasks/{feature-slug}/task.json
+ * .tmp/tasks/{feature-slug}/subtask_01.json
+ * .tmp/tasks/completed/{feature-slug}/
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+// Find project root (look for .git or package.json)
+function findProjectRoot(): string {
+ let dir = process.cwd();
+ while (dir !== path.dirname(dir)) {
+ if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
+ return dir;
+ }
+ dir = path.dirname(dir);
+ }
+ return process.cwd();
+}
+
+const PROJECT_ROOT = findProjectRoot();
+const TASKS_DIR = path.join(PROJECT_ROOT, '.tmp', 'tasks');
+const COMPLETED_DIR = path.join(TASKS_DIR, 'completed');
+
+interface Task {
+ id: string;
+ name: string;
+ status: 'active' | 'completed' | 'blocked' | 'archived';
+ objective: string;
+ context_files: string[];
+ exit_criteria: string[];
+ subtask_count: number;
+ completed_count: number;
+ created_at: string;
+ completed_at: string | null;
+}
+
+interface Subtask {
+ id: string;
+ seq: string;
+ title: string;
+ status: 'pending' | 'in_progress' | 'completed' | 'blocked';
+ depends_on: string[];
+ parallel: boolean;
+ context_files: string[];
+ acceptance_criteria: string[];
+ deliverables: string[];
+ agent_id: string | null;
+ started_at: string | null;
+ completed_at: string | null;
+ completion_summary: string | null;
+}
+
+// Helpers
+function getFeatureDirs(): string[] {
+ if (!fs.existsSync(TASKS_DIR)) return [];
+ return fs.readdirSync(TASKS_DIR).filter((f: string) => {
+ const fullPath = path.join(TASKS_DIR, f);
+ return fs.statSync(fullPath).isDirectory() && f !== 'completed';
+ });
+}
+
+function loadTask(feature: string): Task | null {
+ const taskPath = path.join(TASKS_DIR, feature, 'task.json');
+ if (!fs.existsSync(taskPath)) return null;
+ return JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
+}
+
+function loadSubtasks(feature: string): Subtask[] {
+ const featureDir = path.join(TASKS_DIR, feature);
+ if (!fs.existsSync(featureDir)) return [];
+
+ const files = fs.readdirSync(featureDir)
+ .filter((f: string) => f.match(/^subtask_\d{2}\.json$/))
+ .sort();
+
+ return files.map((f: string) => JSON.parse(fs.readFileSync(path.join(featureDir, f), 'utf-8')));
+}
+
+function saveSubtask(feature: string, subtask: Subtask): void {
+ const subtaskPath = path.join(TASKS_DIR, feature, `subtask_${subtask.seq}.json`);
+ fs.writeFileSync(subtaskPath, JSON.stringify(subtask, null, 2));
+}
+
+function saveTask(feature: string, task: Task): void {
+ const taskPath = path.join(TASKS_DIR, feature, 'task.json');
+ fs.writeFileSync(taskPath, JSON.stringify(task, null, 2));
+}
+
+// Commands
+function cmdStatus(feature?: string): void {
+ const features = feature ? [feature] : getFeatureDirs();
+
+ if (features.length === 0) {
+ console.log('No active features found.');
+ return;
+ }
+
+ for (const f of features) {
+ const task = loadTask(f);
+ const subtasks = loadSubtasks(f);
+
+ if (!task) {
+ console.log(`\n[${f}] - No task.json found`);
+ continue;
+ }
+
+ const counts = {
+ pending: subtasks.filter(s => s.status === 'pending').length,
+ in_progress: subtasks.filter(s => s.status === 'in_progress').length,
+ completed: subtasks.filter(s => s.status === 'completed').length,
+ blocked: subtasks.filter(s => s.status === 'blocked').length,
+ };
+
+ const progress = subtasks.length > 0
+ ? Math.round((counts.completed / subtasks.length) * 100)
+ : 0;
+
+ console.log(`\n[${f}] ${task.name}`);
+ console.log(` Status: ${task.status} | Progress: ${progress}% (${counts.completed}/${subtasks.length})`);
+ console.log(` Pending: ${counts.pending} | In Progress: ${counts.in_progress} | Completed: ${counts.completed} | Blocked: ${counts.blocked}`);
+ }
+}
+
+function cmdNext(feature?: string): void {
+ const features = feature ? [feature] : getFeatureDirs();
+
+ console.log('\n=== Ready Tasks (deps satisfied) ===\n');
+
+ for (const f of features) {
+ const subtasks = loadSubtasks(f);
+ const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
+
+ const ready = subtasks.filter(s => {
+ if (s.status !== 'pending') return false;
+ return s.depends_on.every(dep => completedSeqs.has(dep));
+ });
+
+ if (ready.length > 0) {
+ console.log(`[${f}]`);
+ for (const s of ready) {
+ const parallel = s.parallel ? '[parallel]' : '[sequential]';
+ console.log(` ${s.seq} - ${s.title} ${parallel}`);
+ }
+ console.log();
+ }
+ }
+}
+
+function cmdParallel(feature?: string): void {
+ const features = feature ? [feature] : getFeatureDirs();
+
+ console.log('\n=== Parallelizable Tasks Ready Now ===\n');
+
+ for (const f of features) {
+ const subtasks = loadSubtasks(f);
+ const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
+
+ const parallel = subtasks.filter(s => {
+ if (s.status !== 'pending') return false;
+ if (!s.parallel) return false;
+ return s.depends_on.every(dep => completedSeqs.has(dep));
+ });
+
+ if (parallel.length > 0) {
+ console.log(`[${f}] - ${parallel.length} parallel tasks:`);
+ for (const s of parallel) {
+ console.log(` ${s.seq} - ${s.title}`);
+ }
+ console.log();
+ }
+ }
+}
+
+function cmdDeps(feature: string, seq: string): void {
+ const subtasks = loadSubtasks(feature);
+ const target = subtasks.find(s => s.seq === seq);
+
+ if (!target) {
+ console.log(`Task ${seq} not found in ${feature}`);
+ return;
+ }
+
+ console.log(`\n=== Dependency Tree: ${feature}/${seq} ===\n`);
+ console.log(`${seq} - ${target.title} [${target.status}]`);
+
+ if (target.depends_on.length === 0) {
+ console.log(' └── (no dependencies)');
+ return;
+ }
+
+ const printDeps = (seqs: string[], indent: string = ' '): void => {
+ for (let i = 0; i < seqs.length; i++) {
+ const depSeq = seqs[i];
+ const dep = subtasks.find(s => s.seq === depSeq);
+ const isLast = i === seqs.length - 1;
+ const branch = isLast ? '└──' : '├──';
+
+ if (dep) {
+ const statusIcon = dep.status === 'completed' ? '✓' : dep.status === 'in_progress' ? '~' : '○';
+ console.log(`${indent}${branch} ${statusIcon} ${depSeq} - ${dep.title} [${dep.status}]`);
+ if (dep.depends_on.length > 0) {
+ const newIndent = indent + (isLast ? ' ' : '│ ');
+ printDeps(dep.depends_on, newIndent);
+ }
+ } else {
+ console.log(`${indent}${branch} ? ${depSeq} - NOT FOUND`);
+ }
+ }
+ };
+
+ printDeps(target.depends_on);
+}
+
+function cmdBlocked(feature?: string): void {
+ const features = feature ? [feature] : getFeatureDirs();
+
+ console.log('\n=== Blocked Tasks ===\n');
+
+ for (const f of features) {
+ const subtasks = loadSubtasks(f);
+ const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq));
+
+ const blocked = subtasks.filter(s => {
+ if (s.status === 'blocked') return true;
+ if (s.status !== 'pending') return false;
+ return !s.depends_on.every(dep => completedSeqs.has(dep));
+ });
+
+ if (blocked.length > 0) {
+ console.log(`[${f}]`);
+ for (const s of blocked) {
+ const waitingFor = s.depends_on.filter(dep => !completedSeqs.has(dep));
+ const reason = s.status === 'blocked'
+ ? 'explicitly blocked'
+ : `waiting: ${waitingFor.join(', ')}`;
+ console.log(` ${s.seq} - ${s.title} (${reason})`);
+ }
+ console.log();
+ }
+ }
+}
+
+function cmdComplete(feature: string, seq: string, summary: string): void {
+ if (summary.length > 200) {
+ console.log('Error: Summary must be max 200 characters');
+ process.exit(1);
+ }
+
+ const subtasks = loadSubtasks(feature);
+ const subtask = subtasks.find(s => s.seq === seq);
+
+ if (!subtask) {
+ console.log(`Task ${seq} not found in ${feature}`);
+ process.exit(1);
+ }
+
+ subtask.status = 'completed';
+ subtask.completed_at = new Date().toISOString();
+ subtask.completion_summary = summary;
+
+ saveSubtask(feature, subtask);
+
+ // Update task.json counts
+ const task = loadTask(feature);
+ if (task) {
+ const newSubtasks = loadSubtasks(feature);
+ task.completed_count = newSubtasks.filter(s => s.status === 'completed').length;
+ saveTask(feature, task);
+ }
+
+ console.log(`\n✓ Marked ${feature}/${seq} as completed`);
+ console.log(` Summary: ${summary}`);
+
+ if (task) {
+ console.log(` Progress: ${task.completed_count}/${task.subtask_count}`);
+ }
+}
+
+function cmdValidate(feature?: string): void {
+ const features = feature ? [feature] : getFeatureDirs();
+ let hasErrors = false;
+
+ console.log('\n=== Validation Results ===\n');
+
+ for (const f of features) {
+ const errors: string[] = [];
+ const warnings: string[] = [];
+
+ // Check task.json exists
+ const task = loadTask(f);
+ if (!task) {
+ errors.push('Missing task.json');
+ }
+
+ // Load and validate subtasks
+ const subtasks = loadSubtasks(f);
+ const seqs = new Set(subtasks.map(s => s.seq));
+
+ for (const s of subtasks) {
+ // Check ID format
+ if (!s.id.startsWith(f)) {
+ errors.push(`${s.seq}: ID should start with feature name`);
+ }
+
+ // Check for missing dependencies
+ for (const dep of s.depends_on) {
+ if (!seqs.has(dep)) {
+ errors.push(`${s.seq}: depends on non-existent task ${dep}`);
+ }
+ }
+
+ // Check for circular dependencies
+ const visited = new Set();
+ const checkCircular = (seq: string, path: string[]): boolean => {
+ if (path.includes(seq)) {
+ errors.push(`${s.seq}: circular dependency detected: ${[...path, seq].join(' -> ')}`);
+ return true;
+ }
+ if (visited.has(seq)) return false;
+ visited.add(seq);
+
+ const task = subtasks.find(t => t.seq === seq);
+ if (task) {
+ for (const dep of task.depends_on) {
+ if (checkCircular(dep, [...path, seq])) return true;
+ }
+ }
+ return false;
+ };
+ checkCircular(s.seq, []);
+
+ // Warnings
+ if (s.acceptance_criteria.length === 0) {
+ warnings.push(`${s.seq}: No acceptance criteria defined`);
+ }
+ if (s.deliverables.length === 0) {
+ warnings.push(`${s.seq}: No deliverables defined`);
+ }
+ }
+
+ // Check counts match
+ if (task && task.subtask_count !== subtasks.length) {
+ errors.push(`task.json subtask_count (${task.subtask_count}) doesn't match actual count (${subtasks.length})`);
+ }
+
+ // Print results
+ console.log(`[${f}]`);
+ if (errors.length === 0 && warnings.length === 0) {
+ console.log(' ✓ All checks passed');
+ } else {
+ for (const e of errors) {
+ console.log(` ✗ ERROR: ${e}`);
+ hasErrors = true;
+ }
+ for (const w of warnings) {
+ console.log(` ⚠ WARNING: ${w}`);
+ }
+ }
+ console.log();
+ }
+
+ process.exit(hasErrors ? 1 : 0);
+}
+
+// Main
+const [,, command, ...args] = process.argv;
+
+switch (command) {
+ case 'status':
+ cmdStatus(args[0]);
+ break;
+ case 'next':
+ cmdNext(args[0]);
+ break;
+ case 'parallel':
+ cmdParallel(args[0]);
+ break;
+ case 'deps':
+ if (args.length < 2) {
+ console.log('Usage: deps ');
+ process.exit(1);
+ }
+ cmdDeps(args[0], args[1]);
+ break;
+ case 'blocked':
+ cmdBlocked(args[0]);
+ break;
+ case 'complete':
+ if (args.length < 3) {
+ console.log('Usage: complete "summary"');
+ process.exit(1);
+ }
+ cmdComplete(args[0], args[1], args.slice(2).join(' '));
+ break;
+ case 'validate':
+ cmdValidate(args[0]);
+ break;
+ default:
+ console.log(`
+Task Management CLI
+
+Usage: npx ts-node task-cli.ts [feature] [args...]
+
+Task files are stored in: .tmp/tasks/{feature-slug}/
+
+Commands:
+ status [feature] Show task status summary
+ next [feature] Show next eligible tasks (deps satisfied)
+ parallel [feature] Show parallelizable tasks ready to run
+ deps Show dependency tree for a task
+ blocked [feature] Show blocked tasks and why
+ complete "summary" Mark task completed with summary
+ validate [feature] Validate JSON files and dependencies
+
+Examples:
+ npx ts-node task-cli.ts status
+ npx ts-node task-cli.ts next my-feature
+ npx ts-node task-cli.ts complete my-feature 02 "Implemented auth module"
+`);
+}
diff --git a/evals/framework/scripts/debug/test-debug.sh b/evals/framework/scripts/debug/test-debug.sh
index 0c45747..0b06627 100755
--- a/evals/framework/scripts/debug/test-debug.sh
+++ b/evals/framework/scripts/debug/test-debug.sh
@@ -1,3 +1,3 @@
#!/bin/bash
-cd evals/framework
+cd evals/framework || exit
DEBUG_VERBOSE=true npm run eval:sdk -- --agent=openagent --pattern="smoke-test.yaml" --debug 2>&1 | head -300
diff --git a/evals/framework/scripts/utils/run-tests-batch.sh b/evals/framework/scripts/utils/run-tests-batch.sh
index e0b0ce4..a9b26ac 100755
--- a/evals/framework/scripts/utils/run-tests-batch.sh
+++ b/evals/framework/scripts/utils/run-tests-batch.sh
@@ -47,7 +47,7 @@ for TEST_FILE in $TEST_FILES; do
BATCH_COUNT=$((BATCH_COUNT + 1))
# Run batch when it reaches BATCH_SIZE or is the last file
- if [ $BATCH_COUNT -eq $BATCH_SIZE ] || [ $(echo "$TEST_FILES" | grep -c "$TEST_FILE") -eq $TOTAL_TESTS ]; then
+ if [ $BATCH_COUNT -eq $BATCH_SIZE ] || [ "$(echo "$TEST_FILES" | grep -c "$TEST_FILE")" -eq $TOTAL_TESTS ]; then
echo -e "${YELLOW}📦 Running Batch $BATCH_NUM (${#CURRENT_BATCH[@]} tests)${NC}"
echo "----------------------------------------"
diff --git a/evals/results/latest.json b/evals/results/latest.json
index 8a2c7cf..502fc85 100644
--- a/evals/results/latest.json
+++ b/evals/results/latest.json
@@ -1,16 +1,16 @@
{
"meta": {
- "timestamp": "2026-01-11T16:38:34.260Z",
- "agent": "subagents/core/task-manager",
- "model": "opencode/grok-code-fast",
+ "timestamp": "2026-01-11T21:13:52.792Z",
+ "agent": "task-manager",
+ "model": "opencode/minimax-m2.1-free",
"framework_version": "0.1.0",
- "git_commit": "f10c93d"
+ "git_commit": "c8733ee"
},
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
- "duration_ms": 49410,
+ "duration_ms": 62017,
"pass_rate": 0
},
"by_category": {
@@ -24,14 +24,20 @@
"id": "core-task-manager-smoke-test",
"category": "developer",
"passed": false,
- "duration_ms": 49410,
- "events": 67,
+ "duration_ms": 62017,
+ "events": 32,
"approvals": 0,
"violations": {
- "total": 3,
+ "total": 4,
"errors": 3,
- "warnings": 0,
+ "warnings": 1,
"details": [
+ {
+ "type": "missing-approval",
+ "severity": "error",
+ "message": "Execution tool 'write' called without requesting approval",
+ "expected": true
+ },
{
"type": "missing-approval",
"severity": "error",
@@ -39,14 +45,14 @@
"expected": true
},
{
- "type": "missing-required-tool",
- "severity": "error",
- "message": "Required tool 'bash' was not used"
+ "type": "insufficient-read",
+ "severity": "warning",
+ "message": "Read/execution ratio < 1 (0.50)"
},
{
"type": "missing-required-tool",
"severity": "error",
- "message": "Required tool 'write' was not used"
+ "message": "Required tool 'bash' was not used"
}
]
}
diff --git a/evals/results/serve.sh b/evals/results/serve.sh
index c19e75e..b644c2b 100755
--- a/evals/results/serve.sh
+++ b/evals/results/serve.sh
@@ -6,7 +6,7 @@
PORT=${1:-8000}
TIMEOUT=${2:-15}
-cd "$(dirname "$0")"
+cd "$(dirname "$0")" || exit
echo "🚀 Starting HTTP server on port $PORT..."
echo "📊 Opening dashboard in browser..."
diff --git a/install.sh b/install.sh
index 3a24037..fdf4166 100755
--- a/install.sh
+++ b/install.sh
@@ -328,15 +328,24 @@ resolve_dependencies() {
local id="${component##*:}"
# Get the correct registry key (handles singular/plural)
- local registry_key=$(get_registry_key "$type")
+ local registry_key
+ registry_key=$(get_registry_key "$type")
# Get dependencies for this component
- local deps=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\") | .dependencies[]?" "$TEMP_DIR/registry.json" 2>/dev/null || echo "")
+ local deps
+ deps=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\") | .dependencies[]?" "$TEMP_DIR/registry.json" 2>/dev/null || echo "")
if [ -n "$deps" ]; then
for dep in $deps; do
# Add dependency if not already in list
- if [[ ! " ${SELECTED_COMPONENTS[@]} " =~ " ${dep} " ]]; then
+ local found=0
+ for existing in "${SELECTED_COMPONENTS[@]}"; do
+ if [ "$existing" = "$dep" ]; then
+ found=1
+ break
+ fi
+ done
+ if [ "$found" -eq 0 ]; then
SELECTED_COMPONENTS+=("$dep")
# Recursively resolve dependencies
resolve_dependencies "$dep"
@@ -381,7 +390,8 @@ show_install_location_menu() {
clear
print_header
- local global_path=$(get_global_install_path)
+ local global_path
+ global_path=$(get_global_install_path)
echo -e "${BOLD}Choose installation location:${NC}\n"
echo -e " ${GREEN}1) Local${NC} - Install to ${CYAN}.opencode/${NC} in current directory"
@@ -427,7 +437,8 @@ show_install_location_menu() {
return
fi
- local normalized_path=$(normalize_and_validate_path "$custom_path")
+ local normalized_path
+ normalized_path=$(normalize_and_validate_path "$custom_path")
if [ $? -ne 0 ]; then
print_error "Invalid path"
@@ -495,17 +506,23 @@ show_profile_menu() {
echo -e "${BOLD}Available Installation Profiles:${NC}\n"
# Essential profile
- local essential_name=$(jq_exec '.profiles.essential.name' "$TEMP_DIR/registry.json")
- local essential_desc=$(jq_exec '.profiles.essential.description' "$TEMP_DIR/registry.json")
- local essential_count=$(jq_exec '.profiles.essential.components | length' "$TEMP_DIR/registry.json")
+ local essential_name
+ essential_name=$(jq_exec '.profiles.essential.name' "$TEMP_DIR/registry.json")
+ local essential_desc
+ essential_desc=$(jq_exec '.profiles.essential.description' "$TEMP_DIR/registry.json")
+ local essential_count
+ essential_count=$(jq_exec '.profiles.essential.components | length' "$TEMP_DIR/registry.json")
echo -e " ${GREEN}1) ${essential_name}${NC}"
echo -e " ${essential_desc}"
echo -e " Components: ${essential_count}\n"
# Developer profile
- local dev_desc=$(jq_exec '.profiles.developer.description' "$TEMP_DIR/registry.json")
- local dev_count=$(jq_exec '.profiles.developer.components | length' "$TEMP_DIR/registry.json")
- local dev_badge=$(jq_exec '.profiles.developer.badge // ""' "$TEMP_DIR/registry.json")
+ local dev_desc
+ dev_desc=$(jq_exec '.profiles.developer.description' "$TEMP_DIR/registry.json")
+ local dev_count
+ dev_count=$(jq_exec '.profiles.developer.components | length' "$TEMP_DIR/registry.json")
+ local dev_badge
+ dev_badge=$(jq_exec '.profiles.developer.badge // ""' "$TEMP_DIR/registry.json")
if [ -n "$dev_badge" ]; then
echo -e " ${BLUE}2) Developer ${GREEN}[${dev_badge}]${NC}"
else
@@ -515,25 +532,34 @@ show_profile_menu() {
echo -e " Components: ${dev_count}\n"
# Business profile
- local business_name=$(jq_exec '.profiles.business.name' "$TEMP_DIR/registry.json")
- local business_desc=$(jq_exec '.profiles.business.description' "$TEMP_DIR/registry.json")
- local business_count=$(jq_exec '.profiles.business.components | length' "$TEMP_DIR/registry.json")
+ local business_name
+ business_name=$(jq_exec '.profiles.business.name' "$TEMP_DIR/registry.json")
+ local business_desc
+ business_desc=$(jq_exec '.profiles.business.description' "$TEMP_DIR/registry.json")
+ local business_count
+ business_count=$(jq_exec '.profiles.business.components | length' "$TEMP_DIR/registry.json")
echo -e " ${CYAN}3) ${business_name}${NC}"
echo -e " ${business_desc}"
echo -e " Components: ${business_count}\n"
# Full profile
- local full_name=$(jq_exec '.profiles.full.name' "$TEMP_DIR/registry.json")
- local full_desc=$(jq_exec '.profiles.full.description' "$TEMP_DIR/registry.json")
- local full_count=$(jq_exec '.profiles.full.components | length' "$TEMP_DIR/registry.json")
+ local full_name
+ full_name=$(jq_exec '.profiles.full.name' "$TEMP_DIR/registry.json")
+ local full_desc
+ full_desc=$(jq_exec '.profiles.full.description' "$TEMP_DIR/registry.json")
+ local full_count
+ full_count=$(jq_exec '.profiles.full.components | length' "$TEMP_DIR/registry.json")
echo -e " ${MAGENTA}4) ${full_name}${NC}"
echo -e " ${full_desc}"
echo -e " Components: ${full_count}\n"
# Advanced profile
- local adv_name=$(jq_exec '.profiles.advanced.name' "$TEMP_DIR/registry.json")
- local adv_desc=$(jq_exec '.profiles.advanced.description' "$TEMP_DIR/registry.json")
- local adv_count=$(jq_exec '.profiles.advanced.components | length' "$TEMP_DIR/registry.json")
+ local adv_name
+ adv_name=$(jq_exec '.profiles.advanced.name' "$TEMP_DIR/registry.json")
+ local adv_desc
+ adv_desc=$(jq_exec '.profiles.advanced.description' "$TEMP_DIR/registry.json")
+ local adv_count
+ adv_count=$(jq_exec '.profiles.advanced.components | length' "$TEMP_DIR/registry.json")
echo -e " ${YELLOW}5) ${adv_name}${NC}"
echo -e " ${adv_desc}"
echo -e " Components: ${adv_count}\n"
@@ -560,6 +586,19 @@ show_profile_menu() {
[ -n "$component" ] && SELECTED_COMPONENTS+=("$component")
done < "$temp_file"
+ # Resolve dependencies for profile installs
+ print_step "Resolving dependencies..."
+ local original_count=${#SELECTED_COMPONENTS[@]}
+ for comp in "${SELECTED_COMPONENTS[@]}"; do
+ resolve_dependencies "$comp"
+ done
+
+ local new_count=${#SELECTED_COMPONENTS[@]}
+ if [ "$new_count" -gt "$original_count" ]; then
+ local added=$((new_count - original_count))
+ print_info "Added $added dependencies"
+ fi
+
show_installation_preview
}
@@ -582,8 +621,10 @@ show_custom_menu() {
echo "Available categories:"
for i in "${!categories[@]}"; do
local cat="${categories[$i]}"
- local count=$(jq_exec ".components.${cat} | length" "$TEMP_DIR/registry.json")
- local cat_display=$(echo "$cat" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
+ local count
+ count=$(jq_exec ".components.${cat} | length" "$TEMP_DIR/registry.json")
+ local cat_display
+ cat_display=$(echo "$cat" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
echo " $((i+1))) ${cat_display} (${count} available)"
done
echo " $((${#categories[@]}+1))) Select All"
@@ -628,15 +669,19 @@ show_component_selection() {
local component_details=()
for category in "${categories[@]}"; do
- local cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
+ local cat_display
+ cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
- local components=$(jq_exec ".components.${category}[] | .id" "$TEMP_DIR/registry.json")
+ local components
+ components=$(jq_exec ".components.${category}[] | .id" "$TEMP_DIR/registry.json")
local idx=1
while IFS= read -r comp_id; do
- local comp_name=$(jq_exec ".components.${category}[] | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json")
- local comp_desc=$(jq_exec ".components.${category}[] | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json")
+ local comp_name
+ comp_name=$(jq_exec ".components.${category}[] | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json")
+ local comp_desc
+ comp_desc=$(jq_exec ".components.${category}[] | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json")
echo " ${idx}) ${comp_name}"
echo " ${comp_desc}"
@@ -853,11 +898,14 @@ perform_installation() {
for comp in "${SELECTED_COMPONENTS[@]}"; do
local type="${comp%%:*}"
local id="${comp##*:}"
- local registry_key=$(get_registry_key "$type")
- local path=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
+ local registry_key
+ registry_key=$(get_registry_key "$type")
+ local path
+ path=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
if [ -n "$path" ] && [ "$path" != "null" ]; then
- local install_path=$(get_install_path "$path")
+ local install_path
+ install_path=$(get_install_path "$path")
if [ -f "$install_path" ]; then
collisions+=("$install_path")
fi
@@ -885,7 +933,8 @@ perform_installation() {
# Handle backup strategy
if [ "$install_strategy" = "backup" ]; then
- local backup_dir="${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)"
+ local backup_dir
+ backup_dir="${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)"
print_step "Creating backup..."
# Only backup files that will be overwritten
@@ -924,10 +973,12 @@ perform_installation() {
local id="${comp##*:}"
# Get the correct registry key (handles singular/plural)
- local registry_key=$(get_registry_key "$type")
+ local registry_key
+ registry_key=$(get_registry_key "$type")
# Get component path
- local path=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
+ local path
+ path=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json")
if [ -z "$path" ] || [ "$path" = "null" ]; then
print_warning "Could not find path for ${comp}"
@@ -936,7 +987,8 @@ perform_installation() {
fi
# Convert registry path to installation path
- local dest=$(get_install_path "$path")
+ local dest
+ dest=$(get_install_path "$path")
# Check if file exists before we install (for proper messaging)
local file_existed=false
@@ -984,7 +1036,8 @@ perform_installation() {
# Handle additional paths for advanced profile
if [ "$PROFILE" = "advanced" ]; then
- local additional_paths=$(jq_exec '.profiles.advanced.additionalPaths[]?' "$TEMP_DIR/registry.json")
+ local additional_paths
+ additional_paths=$(jq_exec '.profiles.advanced.additionalPaths[]?' "$TEMP_DIR/registry.json")
if [ -n "$additional_paths" ]; then
print_step "Installing additional paths..."
while IFS= read -r path; do
@@ -1028,7 +1081,22 @@ show_post_install() {
# Show installation location info
print_info "Installation directory: ${CYAN}${INSTALL_DIR}${NC}"
- if [ -d "${INSTALL_DIR}.backup."* ] 2>/dev/null; then
+ # Check for backup directories
+ local has_backup=0
+ local backup_dir
+ local backup_dirs=()
+
+ shopt -s nullglob
+ backup_dirs=("${INSTALL_DIR}.backup."*)
+ shopt -u nullglob
+
+ for backup_dir in "${backup_dirs[@]}"; do
+ if [ -d "$backup_dir" ]; then
+ has_backup=1
+ break
+ fi
+ done
+ if [ "$has_backup" -eq 1 ]; then
print_info "Backup created - you can restore files from ${INSTALL_DIR}.backup.* if needed"
fi
@@ -1051,10 +1119,12 @@ list_components() {
local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts")
for category in "${categories[@]}"; do
- local cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
+ local cat_display
+ cat_display=$(echo "$category" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
echo -e "${CYAN}${BOLD}${cat_display}:${NC}"
- local components=$(jq_exec ".components.${category}[] | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json")
+ local components
+ components=$(jq_exec ".components.${category}[] | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json")
while IFS='|' read -r id name desc; do
echo -e " ${GREEN}${name}${NC} (${id})"
@@ -1202,7 +1272,8 @@ main() {
# Apply custom install directory if specified (CLI arg overrides env var)
if [ -n "$CUSTOM_INSTALL_DIR" ]; then
- local normalized_path=$(normalize_and_validate_path "$CUSTOM_INSTALL_DIR")
+ local normalized_path
+ normalized_path=$(normalize_and_validate_path "$CUSTOM_INSTALL_DIR")
if [ $? -eq 0 ]; then
INSTALL_DIR="$normalized_path"
if ! validate_install_path "$INSTALL_DIR"; then
@@ -1226,6 +1297,20 @@ main() {
while IFS= read -r component; do
[ -n "$component" ] && SELECTED_COMPONENTS+=("$component")
done < "$temp_file"
+
+ # Resolve dependencies for profile installs
+ print_step "Resolving dependencies..."
+ local original_count=${#SELECTED_COMPONENTS[@]}
+ for comp in "${SELECTED_COMPONENTS[@]}"; do
+ resolve_dependencies "$comp"
+ done
+
+ local new_count=${#SELECTED_COMPONENTS[@]}
+ if [ "$new_count" -gt "$original_count" ]; then
+ local added=$((new_count - original_count))
+ print_info "Added $added dependencies"
+ fi
+
show_installation_preview
else
# Interactive mode - show location menu first
diff --git a/packages/plugin-abilities/ARCHITECTURE.md b/packages/plugin-abilities/ARCHITECTURE.md
new file mode 100644
index 0000000..517e35a
--- /dev/null
+++ b/packages/plugin-abilities/ARCHITECTURE.md
@@ -0,0 +1,855 @@
+# Abilities Plugin: Architecture & System Overview
+
+## Executive Summary
+
+The **Abilities Plugin** is an OpenCode plugin that enforces deterministic, step-by-step workflow execution for AI agents. It solves the core problem with traditional skills: **LLMs ignore them**. By using enforcement hooks and structured workflows, Abilities guarantee that agents follow prescribed steps in order, without deviation.
+
+### Core Problem It Solves
+
+```
+Traditional Skills Abilities
+───────────────────────────────── ──────────────────────────
+Agent sees skill definition → Ability enforces execution
+Agent can ignore it → Agent MUST follow steps
+Execution is non-deterministic → Execution is deterministic
+No validation between steps → Each step validated
+Multi-agent coordination fails → Agent-specific abilities work
+```
+
+---
+
+## System Architecture Overview
+
+### High-Level Flow
+
+```
+User Request
+ ↓
+┌─────────────────────────────────┐
+│ OpenCode Chat Message Received │
+ ↓
+┌──────────────────────────────────────┐
+│ AbilitiesPlugin Hooks (opencode-plugin.ts)
+│ ├─ chat.message: Detect ability trigger
+│ ├─ tool.execute.before: Block unauthorized tools
+│ └─ event: Manage execution state
+ ↓
+┌──────────────────────────────────────┐
+│ If ability triggered:
+│ ├─ Load ability definition (loader/)
+│ ├─ Validate inputs (validator/)
+│ └─ Execute steps (executor/)
+ ↓
+┌──────────────────────────────────────┐
+│ Step Execution (ExecutionManager)
+│ ├─ Sequential execution with dependencies
+│ ├─ Output context passing
+│ ├─ Step-level validation
+│ └─ Error handling & recovery
+ ↓
+┌──────────────────────────────────────┐
+│ Enforcement Applied
+│ ├─ Tool blocking (only allowed tools)
+│ ├─ Context injection (ability status)
+│ └─ Step-by-step control
+ ↓
+Agent sees ability context and executes
+as instructed (not as LLM desires)
+```
+
+---
+
+## Module Architecture
+
+```
+src/
+├── opencode-plugin.ts [ENTRY POINT] Main plugin implementation
+│ ├─ Hooks: event, chat.message, tool.execute.before
+│ ├─ Tools: ability.list, ability.run, ability.status, ability.cancel
+│ └─ Enforcement logic & context injection
+│
+├── loader/
+│ └─ index.ts [DISCOVERY] Load ability YAML files
+│ ├─ loadAbilities(): Discover & parse all abilities
+│ ├─ loadAbility(): Get specific ability
+│ └─ listAbilities(): Format for display
+│
+├── validator/
+│ └─ index.ts [VALIDATION] Ensure abilities are valid
+│ ├─ validateAbility(): Check structure, dependencies, step types
+│ ├─ validateInputs(): Type-check user inputs against schema
+│ └─ validateSteps(): Ensure no circular dependencies
+│
+├── executor/
+│ ├─ index.ts [EXECUTION] Run ability steps
+│ │ ├─ executeAbility(): Main orchestrator
+│ │ ├─ executeScriptStep(): Run shell commands
+│ │ ├─ executeAgentStep(): Call agents
+│ │ ├─ executeSkillStep(): Load skills
+│ │ ├─ executeApprovalStep(): Request approval
+│ │ └─ executeWorkflowStep(): Run nested abilities
+│ │
+│ └─ execution-manager.ts [STATE] Track active executions
+│ ├─ ExecutionManager: Lifecycle management
+│ ├─ execute(): Start new execution
+│ ├─ getActive(): Current execution status
+│ ├─ cancelActive(): Stop active ability
+│ └─ cleanup(): GC & resource management
+│
+├── types/
+│ └─ index.ts [TYPES] TypeScript definitions
+│ ├─ Ability, Step types
+│ ├─ Execution state types
+│ └─ Input/output schemas
+│
+└── index.ts [EXPORTS] Public API
+```
+
+---
+
+## Module Responsibilities
+
+### 1. **opencode-plugin.ts** - Main Plugin & Enforcement
+**Responsibility**: Interface between OpenCode and the abilities system
+
+**Key Functions**:
+- `AbilitiesPlugin` - Main async factory function that returns hooks
+- `matchesTrigger()` - Detect if user text matches ability keywords/patterns
+- `detectAbility()` - Find matching ability from user message
+- `showToast()` - Display UI notifications
+- `createExecutorContext()` - Build execution environment
+- `buildAbilityContextInjection()` - Format ability status for agent
+
+**Hooks Implemented**:
+```typescript
+{
+ event() // Handle session lifecycle (create/delete)
+ config() // Load plugin configuration
+ 'chat.message'() // Intercept messages, detect abilities, inject context
+ 'tool.execute.before()' // Block unauthorized tools during steps
+ tool: { // Register custom tools
+ 'ability.list',
+ 'ability.run',
+ 'ability.status',
+ 'ability.cancel'
+ }
+}
+```
+
+**Enforcement Strategy**:
+- **Message Interception**: When user types, check if it matches ability triggers
+- **Tool Blocking**: During ability execution, only allow tools for current step type
+- **Context Injection**: Add ability progress/instructions to every message
+- **State Tracking**: ExecutionManager tracks active executions per session
+
+---
+
+### 2. **loader/index.ts** - Ability Discovery
+**Responsibility**: Find and parse YAML ability definitions from filesystem
+
+**Key Functions**:
+```typescript
+loadAbilities(options) // Discover all abilities in directories
+ └─ discoverAbilities() // Glob for *.yaml files
+ └─ loadAbilityFile() // Parse YAML → Ability object
+
+loadAbility(name) // Get specific ability by name
+
+listAbilities(map) // Format abilities for display
+```
+
+**Globbing Strategy** (Limited scope):
+```typescript
+const ABILITY_PATTERNS = [
+ '*.yaml', // Single-level files
+ '*/ability.yaml', // Dir with ability.yaml
+ '*/*.yaml', // Dir with YAML files
+ '*/*/ability.yaml' // Two-level nesting (max)
+]
+```
+
+**Why Limited Patterns?**
+- Prevents scanning entire project (performance)
+- Stops accidental loading of unrelated YAML files
+- Encourages organized directory structure
+
+**Output**:
+```typescript
+Map {
+ 'deploy': { ability, filePath, source }
+ 'deploy/staging': { ability, filePath, source }
+ 'test-suite': { ability, filePath, source }
+}
+```
+
+---
+
+### 3. **validator/index.ts** - Structure & Input Validation
+**Responsibility**: Ensure abilities are well-formed and inputs are valid
+
+**Key Functions**:
+```typescript
+validateAbility(ability) // Check structure
+ └─ Validates:
+ ├─ name field exists
+ ├─ steps array non-empty
+ ├─ no duplicate step IDs
+ ├─ no circular dependencies
+ ├─ all dependencies exist
+ ├─ step types valid
+ └─ nested abilities exist
+
+validateInputs(ability, inputs) // Type-check user inputs
+ └─ For each input definition:
+ ├─ required field check
+ ├─ type validation (string/number/object)
+ ├─ pattern regex validation
+ ├─ enum value check
+ ├─ min/max range check
+ └─ default value handling
+```
+
+**Validation Output**:
+```typescript
+{
+ valid: boolean
+ errors: Array<{
+ path: string // e.g., "inputs.version"
+ message: string // "Must match pattern: ^v\d+\.\d+\.\d+$"
+ }>
+}
+```
+
+---
+
+### 4. **executor/index.ts** - Step Execution Engine
+**Responsibility**: Execute ability steps sequentially with dependency management
+
+**Key Functions**:
+```typescript
+executeAbility(ability, inputs, ctx, options)
+ └─ buildExecutionOrder(steps) // Resolve dependencies
+ └─ executeStep(step, execution, ctx)
+ ├─ executeScriptStep() // Run: sh -c "command"
+ ├─ executeAgentStep() // Call agent with context
+ ├─ executeSkillStep() // Load skill
+ ├─ executeApprovalStep() // Request user approval
+ └─ executeWorkflowStep() // Run nested ability
+
+formatExecutionResult(execution) // Pretty-print results
+```
+
+**Step Types & Their Allowed Tools**:
+```typescript
+ALLOWED_TOOLS_BY_STEP_TYPE = {
+ script: [], // No tools (runs deterministically)
+ agent: ['task', 'background_task'], // Only agent-calling tools
+ skill: ['skill', 'slashcommand'], // Skill-related tools
+ approval: ['ability.status'], // Read-only status tools
+ workflow: ['ability.run', 'ability.status'] // Run nested ability
+}
+```
+
+**Variable Interpolation**:
+```yaml
+steps:
+ - run: "deploy {{inputs.version}} to {{inputs.env}}"
+ - run: "echo {{steps.test.output}}" # From previous step output
+```
+
+**Dependency Resolution**:
+```yaml
+steps:
+ - id: test
+ type: script
+ run: npm test
+
+ - id: build
+ needs: [test] # Runs after test completes
+ run: npm run build
+
+ - id: deploy
+ needs: [build] # Runs after build completes
+ run: ./deploy.sh
+```
+
+---
+
+### 5. **executor/execution-manager.ts** - Lifecycle Management
+**Responsibility**: Track active executions, manage state, handle cleanup
+
+**Key Responsibilities**:
+```typescript
+class ExecutionManager {
+ // Lifecycle
+ execute(ability, inputs, ctx) // Start new execution
+ getActive() // Get current execution
+ cancelActive() // Stop active ability
+
+ // State Management
+ updateStep(executionId, result) // Mark step complete
+ cancel(executionId) // Cancel by ID
+ get(id) // Retrieve execution history
+ list() // All executions (for debugging)
+
+ // Resource Management
+ cleanup() // Clean up timers & state
+ cleanupOldExecutions() // GC old executions (30 min TTL)
+ trimToMaxSize() // Keep last 50 executions max
+}
+```
+
+**Cleanup Strategy**:
+```typescript
+const EXECUTION_TTL = 30 * 60 * 1000 // Delete after 30 minutes
+const CLEANUP_INTERVAL = 5 * 60 * 1000 // Check every 5 minutes
+const MAX_STORED_EXECUTIONS = 50 // Keep last 50
+```
+
+**Why This Matters**:
+- Lazy timer initialization (doesn't create timers until first execution)
+- Automatic GC prevents memory leaks from long-running sessions
+- State persists across messages in same session
+- Timer uses `unref()` so it doesn't prevent process exit
+
+---
+
+### 6. **types/index.ts** - Type Definitions
+**Responsibility**: Provide TypeScript types for all data structures
+
+**Key Types**:
+```typescript
+// Ability Definition
+interface Ability {
+ name: string
+ description: string
+ triggers?: {
+ keywords?: string[]
+ patterns?: string[] // Regex patterns
+ }
+ inputs?: Record
+ steps: Step[]
+ settings?: {
+ enforcement?: 'strict' | 'normal' | 'loose'
+ on_failure?: 'stop' | 'retry' | 'continue'
+ }
+ exclusive_agent?: string // Only this agent can run
+ compatible_agents?: string[] // Whitelist of agents
+}
+
+// Step Types
+type Step =
+ | ScriptStep
+ | AgentStep
+ | SkillStep
+ | ApprovalStep
+ | WorkflowStep
+
+interface ScriptStep {
+ id: string
+ type: 'script'
+ description?: string
+ run: string // Shell command
+ cwd?: string // Working directory
+ env?: Record // Environment variables
+ timeout?: string // '5m', '30s'
+ validation?: {
+ exit_code?: number
+ stdout_contains?: string
+ stderr_contains?: string
+ }
+ on_failure?: 'stop' | 'retry' | 'continue'
+ max_retries?: number
+ when?: string // Conditional: "inputs.env == 'prod'"
+ needs?: string[] // Dependencies
+}
+
+// Execution State
+interface AbilityExecution {
+ id: string
+ ability: Ability
+ inputs: InputValues
+ status: 'running' | 'completed' | 'failed' | 'cancelled'
+ currentStep: Step | null
+ currentStepIndex: number
+ completedSteps: StepResult[]
+ pendingSteps: Step[]
+ startedAt: number
+ completedAt?: number
+ error?: string
+}
+
+interface StepResult {
+ stepId: string
+ status: 'completed' | 'failed' | 'skipped'
+ output?: string
+ error?: string
+ startedAt: number
+ completedAt: number
+ duration: number
+}
+```
+
+---
+
+## Data Flow Example: Deploy Workflow
+
+### 1. User Types "Deploy v1.2.3"
+
+```
+User Message: "Deploy v1.2.3"
+ ↓
+chat.message hook intercepts
+ ↓
+detectAbility("Deploy v1.2.3")
+ ├─ Check: "deploy" keyword in message? ✓
+ ├─ Found: ability { name: "deploy", ... }
+ ↓
+Show ability suggestion:
+"## Ability Detected: deploy\n\n Deploy application..."
+```
+
+### 2. User Runs: `/ability.run deploy version=v1.2.3`
+
+```
+ability.run tool executes:
+ ├─ Load ability: "deploy"
+ ├─ Validate inputs:
+ │ └─ version matches pattern: ^v\d+\.\d+\.\d+$ ✓
+ ├─ executionManager.execute(ability, {version: "v1.2.3"})
+ │
+ └─ Start execution:
+ ExecutionManager creates AbilityExecution {
+ id: "exec_1704067200000_abc123"
+ status: "running"
+ currentStep: steps[0]
+ }
+```
+
+### 3. Step 1: Test (Script)
+
+```
+Step: "test" (script)
+ ├─ Command: "npm test"
+ ├─ Run in shell:
+ │ ├─ stdout: "✓ 124 tests passed"
+ │ ├─ exit code: 0
+ │ └─ validation: exit_code == 0 ✓
+ ├─ Record result:
+ │ └─ StepResult { stepId: "test", status: "completed", output: "..." }
+ ├─ Inject context in next message:
+ │ "## Active Ability: deploy\nProgress: 1/3 steps\nCurrent Step: build..."
+ └─ Continue
+```
+
+### 4. Step 2: Build (Script, depends on test)
+
+```
+Step: "build" (script)
+ ├─ Needs: ["test"] ✓ (completed)
+ ├─ Command: "npm run build"
+ ├─ Tool check (tool.execute.before):
+ │ └─ Block: bash, write, edit (not allowed in script steps)
+ ├─ Execute...
+ ├─ Result: success
+ └─ Continue
+```
+
+### 5. Step 3: Deploy (Script)
+
+```
+Step: "deploy" (script)
+ ├─ Needs: ["build"] ✓ (completed)
+ ├─ Interpolate variables:
+ │ └─ "Deploy {{inputs.version}}" → "Deploy v1.2.3"
+ ├─ Run: "./deploy.sh v1.2.3"
+ ├─ Result: success
+ └─ Mark ability complete
+```
+
+### 6. Execution Complete
+
+```
+Set status: "completed"
+Save results: { completedSteps: [...], duration: "42.3s" }
+Return: "✅ Ability 'deploy' completed successfully"
+Clear activeExecution for next ability
+```
+
+---
+
+## Enforcement Mechanisms
+
+### 1. Tool Blocking (tool.execute.before hook)
+
+**Problem**: Agent might try to call `bash` during a `script` step (redundant & risky)
+
+**Solution**:
+```typescript
+async 'tool.execute.before'(input, output) {
+ if (!activeExecution) return // Not running ability, allow all
+
+ const currentStep = activeExecution.currentStep
+ const allowedTools = ALLOWED_TOOLS_BY_STEP_TYPE[currentStep.type]
+
+ if (enforcement === 'strict' && !allowedTools.includes(input.tool)) {
+ throw new Error(`Tool '${input.tool}' blocked in ${currentStep.type} step`)
+ }
+}
+```
+
+**Effect**: Agent cannot deviate from prescribed tool usage for current step
+
+### 2. Context Injection (chat.message hook)
+
+**Problem**: Agent might forget which step it's on or what to do next
+
+**Solution**:
+```typescript
+async 'chat.message'(input, output) {
+ if (activeExecution?.status === 'running') {
+ // Inject ability context at start of every message
+ output.parts.unshift({
+ type: 'text',
+ text: `## Active Ability: ${ability.name}\nProgress: 2/3 steps\nCurrent Step: deploy\nAction: Run ./deploy.sh v1.2.3`
+ })
+ }
+}
+```
+
+**Effect**: Agent always sees context reminder, reducing deviation
+
+### 3. Ability Detection (chat.message keyword matching)
+
+**Problem**: User doesn't know they can run an ability
+
+**Solution**:
+```typescript
+const detected = detectAbility(userText) // Check triggers
+if (detected) {
+ output.parts.unshift({
+ type: 'text',
+ text: `## Ability Detected: ${detected.name}\n\n${detected.description}...`
+ })
+}
+```
+
+**Effect**: Auto-discovery makes abilities more discoverable
+
+---
+
+## Configuration
+
+### In `.opencode/opencode.json`:
+
+```json
+{
+ "plugin": [
+ "file://../packages/plugin-abilities/src/opencode-plugin.ts"
+ ]
+}
+```
+
+### Optional Config:
+
+```json
+{
+ "abilities": {
+ "enabled": true,
+ "auto_trigger": true,
+ "enforcement": "strict",
+ "directories": [
+ ".opencode/abilities",
+ "~/.config/opencode/abilities"
+ ]
+ }
+}
+```
+
+---
+
+## Ability File Structure
+
+### Basic Example
+
+```yaml
+# .opencode/abilities/deploy/ability.yaml
+name: deploy
+description: Deploy application with safety checks
+
+triggers:
+ keywords:
+ - deploy
+ - ship
+ patterns:
+ - 'deploy.*v\d+\.\d+\.\d+'
+
+inputs:
+ version:
+ type: string
+ required: true
+ pattern: '^v\d+\.\d+\.\d+$'
+ environment:
+ type: string
+ enum: [dev, staging, prod]
+ default: staging
+
+steps:
+ - id: test
+ type: script
+ run: npm test
+ validation:
+ exit_code: 0
+
+ - id: build
+ type: script
+ needs: [test]
+ run: npm run build
+ validation:
+ exit_code: 0
+
+ - id: approve
+ type: approval
+ needs: [build]
+ prompt: "Deploy {{inputs.version}} to {{inputs.environment}}?"
+
+ - id: deploy
+ type: script
+ needs: [approve]
+ run: ./deploy.sh {{inputs.version}} {{inputs.environment}}
+ validation:
+ exit_code: 0
+```
+
+---
+
+## Tools Available to Agents
+
+### ability.list
+Lists all available abilities
+```
+ability.list
+→ "- deploy: Deploy application...\n- test: Run tests..."
+```
+
+### ability.run
+Execute an ability
+```
+ability.run { name: "deploy", inputs: { version: "v1.2.3" } }
+→ { status: "completed", ability: "deploy", result: "..." }
+```
+
+### ability.status
+Check active ability execution
+```
+ability.status
+→ { status: "running", ability: "deploy", currentStep: "build", progress: "2/3" }
+```
+
+### ability.cancel
+Cancel active ability
+```
+ability.cancel
+→ { status: "cancelled", message: "Ability cancelled" }
+```
+
+---
+
+## Execution Flow Diagram
+
+```
+┌──────────────────────────────────────────────────────────┐
+│ User Message → chat.message hook │
+└────────────────────┬─────────────────────────────────────┘
+ │
+ ┌───────────┴────────────┐
+ ▼ ▼
+ No ability match Ability detected
+ │ │
+ │ ┌─────┴──────┐
+ │ ▼ ▼
+ │ Auto-detect Show suggestion
+ │ (cool 10s) to user
+ │
+ ├─────────────────────────────────────┐
+ │ │
+ Allow normal User runs /ability.run
+ OpenCode flow │
+ ┌──────────┴───────────┐
+ ▼ ▼
+ Load ability Validate inputs
+ │ │
+ └──────────┬───────────┘
+ ▼
+ ExecutionManager.execute()
+ │
+ ┌───────────────┴────────────────┐
+ │ Build execution order (deps) │
+ ├───────────────────────────────┤
+ │ FOR each step: │
+ │ ├─ Evaluate 'when' condition │
+ │ ├─ Execute step type: │
+ │ │ ├─ script → shell cmd │
+ │ │ ├─ agent → agent call │
+ │ │ ├─ skill → load skill │
+ │ │ ├─ approval → ask user │
+ │ │ └─ workflow → nested run │
+ │ ├─ Validate output │
+ │ ├─ Pass context to next step │
+ │ └─ Record result │
+ │ │
+ ├─ On failure: │
+ │ ├─ Stop (default) │
+ │ ├─ Retry (with max retries) │
+ │ └─ Continue (ignore error) │
+ │ │
+ └───────────────┬────────────────┘
+ ▼
+ Return execution results
+ │
+ ┌───────────────────┼───────────────────┐
+ ▼ ▼ ▼
+ Save to history Show toast result Clear active
+ (50 max, 30m TTL) (success/error) execution
+```
+
+---
+
+## Performance & Resource Considerations
+
+### Lazy Initialization
+- ExecutionManager timer only starts on first ability execution
+- Timer uses `unref()` so it doesn't block process exit
+- Prevents unnecessary resource usage for inactive plugins
+
+### Memory Management
+- Keep only last 50 executions in memory
+- Automatically delete executions older than 30 minutes
+- No memory leaks from long-running sessions
+
+### Search Scope
+- Glob patterns limited to 2 levels deep (`*/*/ability.yaml`)
+- Prevents scanning entire project (could be thousands of files)
+- Encourages organized `.opencode/abilities/` directory structure
+
+### Debouncing
+- Ability detection limited to once per 10 seconds per ability
+- Prevents message spam from repeated ability suggestions
+- User can still manually run with `/ability.run`
+
+---
+
+## Extension Points
+
+### Adding New Step Types
+1. Add type definition to `types/index.ts`
+2. Add executor function in `executor/index.ts`
+3. Add to `ALLOWED_TOOLS_BY_STEP_TYPE`
+4. Update validator
+
+### Custom Validation
+1. Extend `validateAbility()` in `validator/index.ts`
+2. Add custom error messages
+3. Return enhanced validation result
+
+### Custom Tools
+1. Add tool definition in `opencode-plugin.ts`
+2. Implement execute function
+3. Register in tool map
+
+### Agent-Specific Abilities
+```yaml
+exclusive_agent: deploy-agent # Only this agent can run
+compatible_agents: [deploy-agent, devops-agent] # Whitelist
+```
+
+---
+
+## Testing
+
+### Test Coverage (87 tests)
+- **executor.test.ts** - Step execution, dependencies, validation
+- **validator.test.ts** - Ability validation, input validation
+- **enforcement.test.ts** - Hook enforcement, agent attachment
+- **integration.test.ts** - Full lifecycle, error handling
+- **trigger.test.ts** - Keyword/pattern detection
+- **context-passing.test.ts** - Output context passing
+- **sdk.test.ts** - Public API
+
+### Running Tests
+```bash
+cd packages/plugin-abilities
+bun test
+```
+
+---
+
+## Troubleshooting
+
+### "Ability not found"
+- Check `.opencode/abilities/` directory exists
+- Check ability YAML file is valid
+- Run `ability.list` to see loaded abilities
+
+### "Input validation failed"
+- Check inputs match schema (type, pattern, enum, range)
+- Use `ability.validate ` to check definition
+
+### "Tool blocked during step"
+- Check enforcement mode (loose vs strict)
+- Tool not in `ALLOWED_TOOLS_BY_STEP_TYPE[stepType]`
+- Script steps block all tools (run deterministically)
+
+### "Step failed but continued"
+- Check `on_failure: continue` in step definition
+- Check `max_retries` configured
+
+### Plugin crashes OpenCode
+- Check hook signatures match SDK (`@opencode-ai/plugin`)
+- Ensure all hooks have try-catch blocks
+- Check console for error messages
+
+---
+
+## Design Philosophy
+
+### Why Enforcement?
+
+> **Hypothesis**: Traditional skills fail because LLMs are optimization engines, not planning engines. They optimize for "completion" not for "following instructions."
+
+**Solution**: Make it impossible to deviate
+- Block tools, not suggest them
+- Inject context, not hope it's remembered
+- Execute steps sequentially, not in parallel
+
+### Why Step-Based?
+
+> **Real World**: Complex tasks have dependencies and validation needs. Humans break them into steps for a reason.
+
+**Solution**: Explicit step dependencies
+- Test before build
+- Build before deploy
+- Approval before production
+
+### Why Validation?
+
+> **Problem**: Without validation, agents "guess" at outputs and continue. This causes silent failures.
+
+**Solution**: Assert expectations after each step
+- Script validation (exit codes, output content)
+- Input validation (required, pattern, range)
+- Dependency validation (no circular loops)
+
+---
+
+## Summary
+
+The Abilities Plugin enforces deterministic, step-by-step workflow execution through:
+
+1. **Discovery** (loader) - Find ability definitions from YAML
+2. **Validation** (validator) - Ensure well-formed and valid inputs
+3. **Execution** (executor) - Run steps sequentially with context passing
+4. **Enforcement** (plugin hooks) - Block tools, inject context, track state
+5. **State Management** (ExecutionManager) - Lifecycle, cleanup, memory management
+
+Result: Agents follow prescribed workflows reliably, reproducibly, and safely.
diff --git a/packages/plugin-abilities/MINIMAL_TEST.md b/packages/plugin-abilities/MINIMAL_TEST.md
new file mode 100644
index 0000000..809ccf3
--- /dev/null
+++ b/packages/plugin-abilities/MINIMAL_TEST.md
@@ -0,0 +1,180 @@
+# Minimal Abilities Plugin - Test Guide
+
+## What Was Simplified
+
+This is a **bare minimum** version to test the core concept:
+
+### ✅ KEPT
+- Script step execution
+- Sequential step ordering (with `needs` dependencies)
+- Tool blocking during execution (enforcement)
+- Context injection into chat messages
+- Input validation
+- Single execution tracking
+
+### ❌ REMOVED
+- `plugin.ts` (duplicate implementation)
+- `sdk.ts` (not needed for testing)
+- Agent/skill/approval/workflow step types
+- Session tracking and multi-session support
+- Agent attachment
+- Triggers and auto-detection
+- Cleanup timers
+- Toast notifications
+- Context passing between steps
+- Conditional execution (`when`)
+
+## File Structure
+
+```
+src/
+ types/index.ts (~120 lines - minimal types)
+ loader/index.ts (unchanged - already simple)
+ validator/index.ts (unchanged - already simple)
+ executor/
+ index.ts (~240 lines - script steps only)
+ execution-manager.ts (~50 lines - single execution)
+ opencode-plugin.ts (~200 lines - minimal hooks)
+ index.ts (exports)
+
+examples/
+ test/ability.yaml (3-step test ability)
+```
+
+**Total: ~600 lines** (down from ~2000+)
+
+## Testing the Core Concept
+
+### Setup
+
+1. **Create test ability directory:**
+ ```bash
+ mkdir -p .opencode/abilities
+ cp examples/test/ability.yaml .opencode/abilities/
+ ```
+
+2. **Configure OpenCode to use plugin:**
+ ```json
+ // .opencode/opencode.json
+ {
+ "plugin": [
+ "file://./packages/plugin-abilities/src/opencode-plugin.ts"
+ ]
+ }
+ ```
+
+### Test 1: Basic Execution
+
+**Goal:** Verify steps execute sequentially
+
+```
+User: ability.run({ name: "test" })
+
+Expected:
+✅ Step 1 executes
+✅ Step 2 executes (after step 1)
+✅ Step 3 executes (after step 2)
+✅ Status = completed
+```
+
+### Test 2: Tool Blocking (CRITICAL)
+
+**Goal:** Verify tools are blocked during script execution
+
+```
+User: ability.run({ name: "test" })
+
+[While step 1 is running]
+User: bash({ command: "ls" })
+
+Expected:
+❌ Error: Tool 'bash' blocked during script step 'step1'
+```
+
+### Test 3: Context Injection
+
+**Goal:** Verify ability context appears in chat
+
+```
+User: ability.run({ name: "test" })
+
+[Send any message while running]
+User: What's happening?
+
+Expected:
+🔄 Active Ability: test
+Progress: 1/3 steps completed
+Current Step: step2
+⚠️ ENFORCEMENT ACTIVE
+```
+
+### Test 4: Status Check
+
+**Goal:** Verify status tool works
+
+```
+User: ability.run({ name: "test" })
+User: ability.status()
+
+Expected:
+{
+ "status": "running",
+ "ability": "test",
+ "currentStep": "step2",
+ "progress": "1/3"
+}
+```
+
+### Test 5: Input Validation
+
+**Goal:** Verify inputs are validated
+
+```
+User: ability.run({ name: "test", inputs: { message: "Custom message" } })
+
+Expected:
+✅ Step 1 output contains "Custom message"
+```
+
+## Success Criteria
+
+All 5 tests pass = **Core concept proven**
+
+Then you can add back:
+- Agent steps
+- Skill steps
+- Session management
+- Context passing
+- Triggers
+- etc.
+
+## Quick Validation
+
+```bash
+# Build
+cd packages/plugin-abilities
+bun run build
+
+# Run minimal test (if you have test runner)
+bun test tests/minimal.test.ts
+```
+
+## What This Proves
+
+1. **Hook-based enforcement works** - Tools can be blocked
+2. **Context injection works** - AI sees ability state
+3. **Sequential execution works** - Steps run in order
+4. **State tracking works** - ExecutionManager tracks progress
+
+If these 4 things work, the architecture is sound.
+
+## Next Steps After Validation
+
+1. Add agent steps (call other agents)
+2. Add session scoping (multi-user support)
+3. Add context passing (step outputs → next step inputs)
+4. Add triggers (auto-detect abilities from keywords)
+5. Add approval steps (human-in-the-loop)
+6. Add workflow steps (nested abilities)
+
+But **test the minimal version first** before adding complexity.
diff --git a/packages/plugin-abilities/QUICK_REFERENCE.md b/packages/plugin-abilities/QUICK_REFERENCE.md
new file mode 100644
index 0000000..31205c9
--- /dev/null
+++ b/packages/plugin-abilities/QUICK_REFERENCE.md
@@ -0,0 +1,320 @@
+# Abilities Plugin - Quick Reference (Minimal Version)
+
+## What Is This?
+
+A **minimal** implementation of enforced workflows for OpenCode agents.
+
+**Core idea:** Force AI to follow predefined steps instead of improvising.
+
+## Quick Start
+
+### 1. Create an Ability
+
+```yaml
+# .opencode/abilities/deploy.yaml
+name: deploy
+description: Deploy with tests
+
+inputs:
+ version:
+ type: string
+ required: true
+
+steps:
+ - id: test
+ type: script
+ run: npm test
+ validation:
+ exit_code: 0
+
+ - id: deploy
+ type: script
+ needs: [test]
+ run: ./deploy.sh {{inputs.version}}
+```
+
+### 2. Run It
+
+```javascript
+ability.run({ name: "deploy", inputs: { version: "v1.0.0" } })
+```
+
+### 3. What Happens
+
+1. ✅ Step "test" runs: `npm test`
+2. ⏸️ **All other tools blocked** until test completes
+3. ✅ Step "deploy" runs: `./deploy.sh v1.0.0`
+4. ✅ Status: completed
+
+## Available Tools
+
+### `ability.list`
+List all loaded abilities
+```javascript
+ability.list()
+// Returns: "- test: Minimal test ability (3 steps)"
+```
+
+### `ability.run`
+Execute an ability
+```javascript
+ability.run({
+ name: "test",
+ inputs: { message: "Hello" }
+})
+```
+
+### `ability.status`
+Check current execution
+```javascript
+ability.status()
+// Returns: { status: "running", currentStep: "step2", progress: "1/3" }
+```
+
+### `ability.cancel`
+Stop active execution
+```javascript
+ability.cancel()
+// Returns: { status: "cancelled" }
+```
+
+## Step Types (Minimal Version)
+
+### Script Step
+Runs shell commands
+
+```yaml
+- id: build
+ type: script
+ run: npm run build
+ validation:
+ exit_code: 0
+```
+
+**Features:**
+- Sequential execution
+- Exit code validation
+- Input variable interpolation: `{{inputs.version}}`
+- Dependency ordering: `needs: [test]`
+
+## Enforcement
+
+### Tool Blocking
+
+When a script step is running, **all tools are blocked** except:
+- `ability.list`
+- `ability.status`
+- `ability.cancel`
+- `read`, `glob`, `grep` (read-only)
+
+**Example:**
+```
+ability.run({ name: "test" })
+[while running]
+bash({ command: "ls" }) // ❌ BLOCKED
+```
+
+### Context Injection
+
+Every chat message shows ability status:
+
+```
+🔄 Active Ability: test
+Progress: 1/3 steps completed
+Current Step: step2
+⚠️ ENFORCEMENT ACTIVE
+```
+
+## File Structure
+
+```
+.opencode/
+ abilities/
+ test.yaml # Your ability
+ deploy.yaml # Another ability
+```
+
+## Ability YAML Schema
+
+```yaml
+name: string # Required: unique name
+description: string # Required: what it does
+
+inputs: # Optional: input parameters
+ param_name:
+ type: string|number|boolean
+ required: boolean
+ default: any
+
+steps: # Required: at least 1 step
+ - id: string # Required: unique step ID
+ type: script # Required: only 'script' in minimal version
+ description: string # Optional: what this step does
+ run: string # Required: shell command
+ needs: [step_ids] # Optional: dependencies
+ validation: # Optional: validation rules
+ exit_code: number # Expected exit code (default: any)
+```
+
+## Input Interpolation
+
+Use `{{inputs.name}}` in commands:
+
+```yaml
+inputs:
+ version:
+ type: string
+ required: true
+
+steps:
+ - id: deploy
+ run: ./deploy.sh {{inputs.version}}
+```
+
+## Step Dependencies
+
+Use `needs` to order steps:
+
+```yaml
+steps:
+ - id: test
+ run: npm test
+
+ - id: build
+ needs: [test] # Runs AFTER test
+ run: npm run build
+
+ - id: deploy
+ needs: [build] # Runs AFTER build
+ run: ./deploy.sh
+```
+
+## Validation
+
+### Input Validation
+
+```yaml
+inputs:
+ count:
+ type: number
+ required: true
+```
+
+If missing: ❌ `Input validation failed: count is required`
+
+### Exit Code Validation
+
+```yaml
+steps:
+ - id: test
+ run: npm test
+ validation:
+ exit_code: 0
+```
+
+If fails: ❌ `Exit code 1, expected 0`
+
+## Testing
+
+### Quick Test
+
+```bash
+cd packages/plugin-abilities
+bun test-minimal.ts
+```
+
+### Manual Test
+
+```yaml
+# .opencode/abilities/hello.yaml
+name: hello
+description: Test ability
+
+steps:
+ - id: greet
+ type: script
+ run: echo "Hello World"
+```
+
+```javascript
+ability.run({ name: "hello" })
+// Output: ✅ greet: completed
+// Output: Hello World
+```
+
+## Limitations (Minimal Version)
+
+**Not Included:**
+- ❌ Agent steps (calling other agents)
+- ❌ Skill steps (loading skills)
+- ❌ Approval steps (human gates)
+- ❌ Workflow steps (nested abilities)
+- ❌ Session management (multi-user)
+- ❌ Triggers (auto-detection)
+- ❌ Context passing (step outputs → next step)
+
+**These will be added after core is validated.**
+
+## Troubleshooting
+
+### "No abilities loaded"
+- Check `.opencode/abilities/` directory exists
+- Check YAML files are valid
+- Check plugin is configured in `opencode.json`
+
+### "Tool blocked during script step"
+- This is **expected** - enforcement is working!
+- Wait for step to complete
+- Or use `ability.cancel()` to stop
+
+### "Input validation failed"
+- Check required inputs are provided
+- Check input types match (string/number/boolean)
+
+### "Already executing ability"
+- Only one ability can run at a time (minimal version)
+- Use `ability.cancel()` to stop current execution
+
+## Next Steps
+
+Once minimal version is validated:
+
+1. Add agent steps (call other agents)
+2. Add session scoping (multi-user)
+3. Add context passing (step outputs)
+4. Add triggers (auto-detection)
+5. Add approval steps (human gates)
+6. Add workflow steps (nested abilities)
+
+## Architecture
+
+```
+User Request
+ ↓
+ability.run tool
+ ↓
+ExecutionManager.execute()
+ ↓
+For each step:
+ ├─ Set as currentStep
+ ├─ Block other tools (tool.execute.before hook)
+ ├─ Inject context (chat.message hook)
+ ├─ Execute script
+ ├─ Validate result
+ └─ Continue or fail
+ ↓
+Return execution result
+```
+
+## Key Files
+
+- `src/opencode-plugin.ts` - Plugin hooks and tools
+- `src/executor/index.ts` - Script execution
+- `src/executor/execution-manager.ts` - State tracking
+- `src/types/index.ts` - TypeScript types
+- `examples/test/ability.yaml` - Example ability
+
+## Support
+
+See `MINIMAL_TEST.md` for detailed testing guide.
+See `SIMPLIFICATION_SUMMARY.md` for what changed.
diff --git a/packages/plugin-abilities/README.md b/packages/plugin-abilities/README.md
new file mode 100644
index 0000000..7c3c934
--- /dev/null
+++ b/packages/plugin-abilities/README.md
@@ -0,0 +1,401 @@
+# @openagents/plugin-abilities
+
+Enforced, validated workflows for OpenCode agents.
+
+## Overview
+
+Abilities solve the fundamental problem with Skills: **LLMs ignore them**. With Abilities:
+
+- Steps **must** run (enforced via hooks)
+- Scripts run **deterministically** (no AI variance)
+- Validation **guarantees** each step completed
+- Multi-agent coordination **just works**
+
+## Installation
+
+### As OpenCode Plugin
+
+Add to your `opencode.json`:
+
+```json
+{
+ "plugin": [
+ "file://./packages/plugin-abilities/src/opencode-plugin.ts"
+ ],
+ "abilities": {
+ "enabled": true,
+ "auto_trigger": true,
+ "enforcement": "strict"
+ }
+}
+```
+
+### As Standalone Package
+
+```bash
+bun add @openagents/plugin-abilities
+```
+
+## Quick Start
+
+### 1. Create an ability
+
+```yaml
+# .opencode/abilities/deploy/ability.yaml
+name: deploy
+description: Deploy with safety checks
+
+triggers:
+ keywords:
+ - "deploy"
+ - "ship it"
+
+inputs:
+ version:
+ type: string
+ required: true
+ pattern: '^v\d+\.\d+\.\d+$'
+
+steps:
+ - id: test
+ type: script
+ run: npm test
+ validation:
+ exit_code: 0
+
+ - id: build
+ type: script
+ run: npm run build
+ needs: [test]
+
+ - id: deploy
+ type: script
+ run: ./deploy.sh {{inputs.version}}
+ needs: [build]
+```
+
+### 2. Run the ability
+
+```bash
+/ability run deploy --version=v1.2.3
+```
+
+Or let it auto-detect from natural language:
+
+```
+User: "Deploy v1.2.3 to production"
+→ Ability detected: deploy
+```
+
+## Step Types
+
+### Script
+
+Runs a shell command deterministically.
+
+```yaml
+- id: test
+ type: script
+ run: npm test
+ cwd: ./packages/api
+ env:
+ NODE_ENV: test
+ validation:
+ exit_code: 0
+ stdout_contains: "passed"
+ timeout: 5m
+ on_failure: stop
+```
+
+### Agent
+
+Calls an OpenAgents agent.
+
+```yaml
+- id: review
+ type: agent
+ agent: reviewer
+ prompt: "Review the code changes"
+ needs: [test]
+```
+
+### Skill
+
+Loads an existing skill.
+
+```yaml
+- id: docs
+ type: skill
+ skill: generate-docs
+```
+
+### Approval
+
+Human approval gate.
+
+```yaml
+- id: approve
+ type: approval
+ prompt: "Deploy to production?"
+ when: inputs.environment == "production"
+```
+
+### Workflow
+
+Nested workflow (calls another ability).
+
+```yaml
+- id: setup
+ type: workflow
+ workflow: setup-environment
+ inputs:
+ env: {{inputs.environment}}
+```
+
+## Input Validation
+
+```yaml
+inputs:
+ version:
+ type: string
+ required: true
+ pattern: '^v\d+\.\d+\.\d+$'
+
+ count:
+ type: number
+ min: 1
+ max: 100
+ default: 10
+
+ env:
+ type: string
+ enum: [dev, staging, prod]
+```
+
+## Dependencies
+
+Steps can depend on other steps:
+
+```yaml
+steps:
+ - id: test
+ type: script
+ run: npm test
+
+ - id: build
+ needs: [test] # Runs after test completes
+ type: script
+ run: npm run build
+
+ - id: deploy
+ needs: [build] # Runs after build completes
+ type: script
+ run: ./deploy.sh
+```
+
+## Conditional Execution
+
+```yaml
+- id: deploy-prod
+ type: script
+ run: ./deploy.sh prod
+ when: inputs.environment == "production"
+```
+
+## Enforcement
+
+Abilities enforce execution order via hooks:
+
+- **strict**: Block ALL tools outside current step (recommended)
+- **normal**: Block destructive tools, warn on others
+- **loose**: Advisory only
+
+```yaml
+settings:
+ enforcement: strict
+```
+
+### How Enforcement Works
+
+1. **tool.execute.before** - Blocks tools based on current step type:
+ - Script steps: Block ALL tools (script runs deterministically)
+ - Agent steps: Allow only agent invocation tools (task, background_task)
+ - Approval steps: Block until user approves
+ - Skill steps: Allow only skill-loading tools
+
+2. **chat.message** - Injects ability context into every message:
+ - Shows current ability name and progress
+ - Shows current step instructions
+ - Reminds AI what to do next
+
+3. **session.idle** - Prevents session exit while ability running:
+ - Injects continuation message
+ - In strict mode, blocks exit until complete
+
+### Always Allowed Tools
+
+These tools are never blocked (read-only/status):
+
+```
+ability.list, ability.status, ability.cancel,
+todoread, read, glob, grep,
+lsp_hover, lsp_diagnostics, lsp_document_symbols
+```
+
+## Agent Attachment
+
+Abilities can be attached to specific agents:
+
+### In Ability Definition
+
+```yaml
+# Restrict to specific agents
+compatible_agents:
+ - deploy-agent
+ - devops-agent
+
+# Or exclusive to one agent
+exclusive_agent: deploy-agent
+```
+
+### Via Agent Frontmatter
+
+```markdown
+---
+name: deploy-agent
+abilities:
+ - deploy/production
+ - deploy/staging
+ - rollback
+---
+```
+
+### Checking Agent Abilities
+
+Use the `ability.agent` tool:
+
+```
+ability.agent({ agent: "deploy-agent" })
+→ Lists all abilities available to that agent
+```
+
+## API
+
+### Tools
+
+- `ability.list` - List available abilities
+- `ability.validate ` - Validate an ability
+- `ability.run [inputs]` - Execute an ability
+- `ability.status` - Get active execution status
+
+### Programmatic (SDK)
+
+```typescript
+import { createAbilitiesSDK } from '@openagents/plugin-abilities/sdk'
+
+// Create SDK instance
+const sdk = createAbilitiesSDK({
+ projectDir: '.opencode/abilities',
+ includeGlobal: true
+})
+
+// List all abilities
+const abilities = await sdk.list()
+// => [{ name, description, source, triggers, inputCount, stepCount }]
+
+// Get specific ability
+const ability = await sdk.get('deploy')
+
+// Validate
+const { valid, errors } = await sdk.validate('deploy')
+
+// Execute with inputs
+const result = await sdk.execute('deploy', { version: 'v1.2.3' })
+// => { id, status, ability, duration, steps, formatted }
+
+// Check status
+const status = await sdk.status()
+// => { active, ability, currentStep, progress, status }
+
+// Cancel active execution
+await sdk.cancel()
+
+// Wait for completion (async)
+const final = await sdk.waitFor(result.id, 60000)
+
+// Cleanup when done
+sdk.cleanup()
+```
+
+### Low-level API
+
+```typescript
+import { loadAbilities, executeAbility, validateAbility } from '@openagents/plugin-abilities'
+
+// Load all abilities
+const abilities = await loadAbilities({
+ projectDir: '.opencode/abilities',
+})
+
+// Validate
+const result = validateAbility(ability)
+
+// Execute
+const execution = await executeAbility(ability, inputs, context)
+```
+
+## File Structure
+
+```
+.opencode/
+└── abilities/
+ ├── deploy/
+ │ └── ability.yaml
+ ├── test-suite/
+ │ └── ability.yaml
+ └── simple.yaml
+```
+
+## Development
+
+### Running Tests
+
+```bash
+cd packages/plugin-abilities
+bun test
+```
+
+**Test Results:** 87 tests passing across 7 test files
+
+### Test Coverage
+
+- `executor.test.ts` - Script execution, step ordering, validation
+- `validator.test.ts` - Ability validation, input validation
+- `enforcement.test.ts` - Hook enforcement, agent attachment
+- `integration.test.ts` - Full lifecycle, ExecutionManager, error handling
+- `trigger.test.ts` - Keyword/pattern matching, auto-detection
+
+### Building
+
+```bash
+bun run build
+```
+
+### Testing Plugin Locally
+
+```bash
+bun test-plugin.ts
+```
+
+## Implementation Status
+
+- [x] Phase 1: Foundation (loader, validator, script executor)
+- [x] Phase 2: Agent Integration (agent/skill/approval steps, triggers)
+- [x] Phase 3: Enforcement (hooks, agent attachment, execution state)
+- [x] Phase 4: Polish (context passing, nested workflows, SDK)
+
+**All phases complete! 87 tests passing.**
+
+## License
+
+MIT
diff --git a/packages/plugin-abilities/SIMPLIFICATION_SUMMARY.md b/packages/plugin-abilities/SIMPLIFICATION_SUMMARY.md
new file mode 100644
index 0000000..a0a1390
--- /dev/null
+++ b/packages/plugin-abilities/SIMPLIFICATION_SUMMARY.md
@@ -0,0 +1,187 @@
+# Abilities Plugin - Simplification Summary
+
+## What We Did
+
+Stripped the plugin down from **~2000+ lines** to **~600 lines** to test the core concept.
+
+## Files Modified
+
+### ✅ Simplified Files
+
+1. **`src/types/index.ts`** (~120 lines, was ~284)
+ - Removed: agent/skill/approval/workflow step types
+ - Kept: script steps, basic validation, execution tracking
+
+2. **`src/executor/execution-manager.ts`** (~50 lines, was ~164)
+ - Removed: session tracking, cleanup timers, multi-execution
+ - Kept: single execution tracking, cancel, cleanup
+
+3. **`src/executor/index.ts`** (~240 lines, was ~700)
+ - Removed: agent/skill/approval/workflow execution
+ - Removed: context passing, summarization, retry logic
+ - Kept: script execution, dependency ordering, validation
+
+4. **`src/opencode-plugin.ts`** (~200 lines, was ~380)
+ - Removed: session management, agent attachment, triggers, toasts
+ - Kept: tool blocking, context injection, basic tools
+
+5. **`src/index.ts`** (~30 lines, was ~10)
+ - Updated exports to match minimal implementation
+
+### ❌ Files to Delete (Not Needed for Testing)
+
+- `src/plugin.ts` - Duplicate implementation (802 lines)
+- `src/sdk.ts` - SDK wrapper (not needed for core test)
+- Most test files (will recreate minimal ones)
+
+### ✅ New Files Created
+
+1. **`examples/test/ability.yaml`** - Simple 3-step test ability
+2. **`test-minimal.ts`** - Quick validation script
+3. **`MINIMAL_TEST.md`** - Testing guide
+4. **`SIMPLIFICATION_SUMMARY.md`** - This file
+
+## What Works Now
+
+✅ **Core functionality proven:**
+
+```bash
+$ bun test-minimal.ts
+
+✅ Loaded ability: test
+✅ Ability is valid
+✅ step1: completed
+✅ step2: completed
+✅ step3: completed
+✅ All tests passed!
+```
+
+## What to Test Next
+
+### In OpenCode Environment
+
+1. **Tool Blocking Test**
+ ```
+ ability.run({ name: "test" })
+ [while running] bash({ command: "ls" })
+ Expected: ❌ Tool blocked
+ ```
+
+2. **Context Injection Test**
+ ```
+ ability.run({ name: "test" })
+ [send message while running]
+ Expected: See "🔄 Active Ability: test"
+ ```
+
+3. **Status Check Test**
+ ```
+ ability.status()
+ Expected: Current step info
+ ```
+
+## Architecture Validation
+
+### ✅ Proven Concepts
+
+1. **Hook-based enforcement** - `tool.execute.before` can block tools
+2. **State tracking** - ExecutionManager tracks current step
+3. **Sequential execution** - Steps run in dependency order
+4. **Context injection** - `chat.message` hook injects ability state
+
+### 🔄 Still to Validate in Real Environment
+
+1. Does tool blocking actually prevent AI from using tools?
+2. Does context injection keep AI on track?
+3. Does the plugin load correctly in OpenCode?
+
+## Next Steps
+
+### Phase 1: Validate Minimal (NOW)
+- [ ] Test in OpenCode environment
+- [ ] Verify tool blocking works
+- [ ] Verify context injection works
+- [ ] Verify status tracking works
+
+### Phase 2: Add Back Features (LATER)
+Once core is proven, add back incrementally:
+- [ ] Agent steps (call other agents)
+- [ ] Session scoping (multi-user)
+- [ ] Context passing (step outputs)
+- [ ] Triggers (auto-detection)
+- [ ] Approval steps
+- [ ] Workflow steps (nested abilities)
+
+## Key Insights from Simplification
+
+### 1. Event Architecture Issues (FIXED)
+**Problem:** Two plugin implementations with different event handling
+**Solution:** Kept one simple implementation, removed session complexity
+
+### 2. State Management Issues (FIXED)
+**Problem:** Global execution state across all sessions
+**Solution:** Simplified to single execution (will add session scoping later)
+
+### 3. Over-Engineering (FIXED)
+**Problem:** Too many features before proving core concept
+**Solution:** Stripped to essentials - script steps only
+
+## File Size Comparison
+
+```
+Before:
+- types/index.ts: 284 lines
+- executor/index.ts: 700 lines
+- executor/execution-manager: 164 lines
+- opencode-plugin.ts: 380 lines
+- plugin.ts: 802 lines (duplicate!)
+- sdk.ts: ~200 lines
+Total: ~2500+ lines
+
+After:
+- types/index.ts: 120 lines
+- executor/index.ts: 240 lines
+- executor/execution-manager: 50 lines
+- opencode-plugin.ts: 200 lines
+Total: ~600 lines
+```
+
+**76% reduction** while keeping all core functionality!
+
+## Testing Checklist
+
+- [x] Load abilities from YAML
+- [x] Validate ability structure
+- [x] Execute script steps sequentially
+- [x] Respect step dependencies (`needs`)
+- [x] Validate exit codes
+- [x] Interpolate input variables
+- [ ] Block tools during execution (needs OpenCode)
+- [ ] Inject context into chat (needs OpenCode)
+- [ ] Track execution status (needs OpenCode)
+
+## Success Criteria
+
+**Minimal version is successful if:**
+
+1. ✅ Abilities load from YAML
+2. ✅ Steps execute in order
+3. ✅ Dependencies are respected
+4. ⏳ Tools are blocked during execution
+5. ⏳ Context appears in chat messages
+6. ⏳ Status tracking works
+
+**3/6 proven in isolation, 3/6 need OpenCode environment**
+
+## Conclusion
+
+The simplification was successful. We now have a **testable minimal implementation** that proves the core concept without the complexity of:
+
+- Session management
+- Multi-execution tracking
+- Agent/skill/approval steps
+- Triggers and auto-detection
+- Cleanup timers
+- Toast notifications
+
+**Next:** Test in OpenCode environment to validate hooks work as expected.
diff --git a/packages/plugin-abilities/bun.lock b/packages/plugin-abilities/bun.lock
new file mode 100644
index 0000000..3b27039
--- /dev/null
+++ b/packages/plugin-abilities/bun.lock
@@ -0,0 +1,122 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "@openagents/plugin-abilities",
+ "dependencies": {
+ "@opencode-ai/plugin": "^1.0.223",
+ "glob": "^10.3.10",
+ "yaml": "^2.3.4",
+ "zod": "^3.22.4",
+ },
+ "devDependencies": {
+ "@types/bun": "^1.0.0",
+ "@types/node": "^20.10.0",
+ "typescript": "^5.3.0",
+ },
+ },
+ },
+ "packages": {
+ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
+
+ "@opencode-ai/plugin": ["@opencode-ai/plugin@1.0.223", "", { "dependencies": { "@opencode-ai/sdk": "1.0.223", "zod": "4.1.8" } }, "sha512-ZQAB7woEWHTpDlZrr+WYwIFI/QrmPblGk1nYLRObtpdMFoP8e2zLwE61j0IL4eBrgWY23+Xc2MrALZnkWL4O2Q=="],
+
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.0.223", "", {}, "sha512-oKJ6QjsviE+lt6cpGu0lL2kWuoj84ZkWvwieyqHEQ2pJunAJqUzhmIhzep0QyDax1/+UXhBWfrnciNt48ch66w=="],
+
+ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+
+ "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
+
+ "@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
+
+ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+
+ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
+
+ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+
+ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
+
+ "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
+
+ "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
+ "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+
+ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+
+ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
+ "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+
+ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
+
+ "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
+
+ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
+
+ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
+ "@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
+
+ "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ }
+}
diff --git a/packages/plugin-abilities/docs/GETTING_STARTED.md b/packages/plugin-abilities/docs/GETTING_STARTED.md
new file mode 100644
index 0000000..5728743
--- /dev/null
+++ b/packages/plugin-abilities/docs/GETTING_STARTED.md
@@ -0,0 +1,177 @@
+# Getting Started with Abilities
+
+Abilities are enforced, validated workflows that guarantee step execution. Unlike skills which LLMs can ignore, abilities enforce execution order via hooks.
+
+## Quick Start
+
+### 1. Create Your First Ability
+
+Create `.opencode/abilities/my-first/ability.yaml`:
+
+```yaml
+name: my-first
+description: My first ability
+
+steps:
+ - id: greet
+ type: script
+ run: echo "Hello from abilities!"
+```
+
+### 2. Run It
+
+```bash
+# In OpenCode CLI
+/ability run my-first
+
+# Or via the SDK
+import { createAbilitiesSDK } from '@openagents/plugin-abilities/sdk'
+
+const sdk = createAbilitiesSDK({ projectDir: '.opencode/abilities' })
+const result = await sdk.execute('my-first')
+console.log(result.formatted)
+```
+
+## Adding Inputs
+
+```yaml
+name: greet
+description: Greeting with custom name
+
+inputs:
+ name:
+ type: string
+ required: true
+ default: "World"
+
+steps:
+ - id: greet
+ type: script
+ run: echo "Hello {{inputs.name}}!"
+```
+
+Usage: `/ability run greet --name=Alice`
+
+## Step Types
+
+### Script Steps
+
+Run shell commands deterministically:
+
+```yaml
+- id: test
+ type: script
+ run: npm test
+ validation:
+ exit_code: 0
+```
+
+### Agent Steps
+
+Invoke other agents:
+
+```yaml
+- id: review
+ type: agent
+ agent: reviewer
+ prompt: "Review the changes for security issues"
+```
+
+### Skill Steps
+
+Load existing skills:
+
+```yaml
+- id: docs
+ type: skill
+ skill: generate-docs
+```
+
+### Approval Steps
+
+Request user approval:
+
+```yaml
+- id: confirm
+ type: approval
+ prompt: "Ready to deploy to production?"
+```
+
+### Workflow Steps
+
+Call nested abilities:
+
+```yaml
+- id: setup
+ type: workflow
+ workflow: setup-environment
+```
+
+## Step Dependencies
+
+Use `needs` to define execution order:
+
+```yaml
+steps:
+ - id: test
+ type: script
+ run: npm test
+
+ - id: build
+ type: script
+ run: npm run build
+ needs: [test] # Runs after test
+
+ - id: deploy
+ type: script
+ run: ./deploy.sh
+ needs: [build] # Runs after build
+```
+
+## Conditional Execution
+
+Use `when` to conditionally skip steps:
+
+```yaml
+- id: deploy-prod
+ type: script
+ run: ./deploy.sh prod
+ when: inputs.environment == "production"
+```
+
+## Enforcement Modes
+
+Configure in `opencode.json`:
+
+```json
+{
+ "abilities": {
+ "enforcement": "strict"
+ }
+}
+```
+
+| Mode | Behavior |
+|------|----------|
+| `strict` | Block ALL tools outside current step |
+| `normal` | Block destructive tools, warn on others |
+| `loose` | Advisory only |
+
+## Auto-Triggering
+
+Define keywords to auto-detect abilities:
+
+```yaml
+name: deploy
+triggers:
+ keywords:
+ - "deploy"
+ - "ship it"
+```
+
+When a user says "deploy to production", the ability is suggested automatically.
+
+## Next Steps
+
+- [YAML Reference](./YAML_REFERENCE.md) - Complete ability format documentation
+- [SDK Reference](./SDK_REFERENCE.md) - Programmatic API
diff --git a/packages/plugin-abilities/examples/deploy/ability.yaml b/packages/plugin-abilities/examples/deploy/ability.yaml
new file mode 100644
index 0000000..3779d40
--- /dev/null
+++ b/packages/plugin-abilities/examples/deploy/ability.yaml
@@ -0,0 +1,72 @@
+name: deploy
+description: Deploy application with safety checks
+
+triggers:
+ keywords:
+ - "deploy"
+ - "ship it"
+ - "release"
+
+inputs:
+ version:
+ type: string
+ required: true
+ pattern: '^v\d+\.\d+\.\d+$'
+ description: "Version to deploy (e.g., v1.2.3)"
+
+ environment:
+ type: string
+ required: true
+ enum: [staging, production]
+ default: staging
+ description: "Target environment"
+
+steps:
+ - id: test
+ type: script
+ description: Run test suite
+ run: echo "Running tests..." && echo "All tests passed"
+ validation:
+ exit_code: 0
+
+ - id: build
+ type: script
+ description: Build application
+ run: echo "Building for {{inputs.environment}}..." && echo "Build complete"
+ needs: [test]
+ validation:
+ exit_code: 0
+
+ - id: deploy-staging
+ type: script
+ description: Deploy to staging
+ run: echo "Deploying {{inputs.version}} to staging..." && echo "Staging deploy complete"
+ needs: [build]
+ validation:
+ exit_code: 0
+
+ - id: approve
+ type: approval
+ description: Approve production deployment
+ when: inputs.environment == "production"
+ prompt: |
+ Ready to deploy {{inputs.version}} to production.
+
+ Staging deployment was successful.
+
+ Proceed with production deployment?
+ needs: [deploy-staging]
+
+ - id: deploy-prod
+ type: script
+ description: Deploy to production
+ when: inputs.environment == "production"
+ run: echo "Deploying {{inputs.version}} to production..." && echo "Production deploy complete"
+ needs: [approve]
+ validation:
+ exit_code: 0
+
+settings:
+ timeout: 30m
+ enforcement: strict
+ on_failure: stop
diff --git a/packages/plugin-abilities/examples/test-suite/ability.yaml b/packages/plugin-abilities/examples/test-suite/ability.yaml
new file mode 100644
index 0000000..09cb641
--- /dev/null
+++ b/packages/plugin-abilities/examples/test-suite/ability.yaml
@@ -0,0 +1,44 @@
+name: test-suite
+description: Run test suite with validation
+
+triggers:
+ keywords:
+ - "run tests"
+ - "test the code"
+ - "check tests"
+
+inputs:
+ coverage:
+ type: boolean
+ required: false
+ default: false
+ description: "Enable coverage reporting"
+
+steps:
+ - id: lint
+ type: script
+ description: Run linter
+ run: echo "Running lint..." && sleep 1 && echo "Lint passed"
+ validation:
+ exit_code: 0
+
+ - id: typecheck
+ type: script
+ description: Run type checker
+ run: echo "Running typecheck..." && sleep 1 && echo "Types OK"
+ needs: [lint]
+ validation:
+ exit_code: 0
+
+ - id: test
+ type: script
+ description: Run tests
+ run: echo "Running tests..." && sleep 2 && echo "All 42 tests passed"
+ needs: [typecheck]
+ validation:
+ exit_code: 0
+ stdout_contains: "passed"
+
+settings:
+ timeout: 10m
+ enforcement: strict
diff --git a/packages/plugin-abilities/examples/test/ability.yaml b/packages/plugin-abilities/examples/test/ability.yaml
new file mode 100644
index 0000000..e039800
--- /dev/null
+++ b/packages/plugin-abilities/examples/test/ability.yaml
@@ -0,0 +1,32 @@
+name: test
+description: Minimal test ability to validate core concept
+
+inputs:
+ message:
+ type: string
+ required: false
+ default: "Hello from abilities!"
+
+steps:
+ - id: step1
+ type: script
+ description: First test step
+ run: echo "Step 1 - {{inputs.message}}"
+ validation:
+ exit_code: 0
+
+ - id: step2
+ type: script
+ description: Second test step (depends on step1)
+ needs: [step1]
+ run: echo "Step 2 - Completed successfully"
+ validation:
+ exit_code: 0
+
+ - id: step3
+ type: script
+ description: Final step
+ needs: [step2]
+ run: echo "Step 3 - All done!"
+ validation:
+ exit_code: 0
diff --git a/packages/plugin-abilities/package.json b/packages/plugin-abilities/package.json
new file mode 100644
index 0000000..2b2b386
--- /dev/null
+++ b/packages/plugin-abilities/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "@openagents/plugin-abilities",
+ "version": "0.1.0",
+ "description": "Enforced, validated workflows for OpenCode agents",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
+ },
+ "./plugin": {
+ "types": "./dist/plugin.d.ts",
+ "import": "./dist/plugin.js"
+ },
+ "./opencode": {
+ "types": "./dist/opencode-plugin.d.ts",
+ "import": "./dist/opencode-plugin.js"
+ },
+ "./sdk": {
+ "types": "./dist/sdk.d.ts",
+ "import": "./dist/sdk.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc",
+ "dev": "tsc --watch",
+ "test": "bun test",
+ "lint": "eslint src/",
+ "clean": "rm -rf dist"
+ },
+ "keywords": [
+ "opencode",
+ "plugin",
+ "abilities",
+ "workflows",
+ "agents"
+ ],
+ "author": "OpenAgents",
+ "license": "MIT",
+ "dependencies": {
+ "@opencode-ai/plugin": "^1.0.223",
+ "glob": "^10.3.10",
+ "yaml": "^2.3.4",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@types/bun": "^1.0.0",
+ "@types/node": "^20.10.0",
+ "typescript": "^5.3.0"
+ },
+ "peerDependencies": {},
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "engines": {
+ "node": ">=18.0.0"
+ }
+}
diff --git a/packages/plugin-abilities/src/executor/execution-manager.ts b/packages/plugin-abilities/src/executor/execution-manager.ts
new file mode 100644
index 0000000..a424eb2
--- /dev/null
+++ b/packages/plugin-abilities/src/executor/execution-manager.ts
@@ -0,0 +1,59 @@
+import type { Ability, AbilityExecution, ExecutorContext } from '../types/index.js'
+import { executeAbility } from './index.js'
+
+/**
+ * Minimal ExecutionManager
+ *
+ * Simplified to track SINGLE execution at a time.
+ * No session management, no cleanup timers, no multi-execution.
+ *
+ * This is the bare minimum to test the core concept.
+ */
+export class ExecutionManager {
+ private activeExecution: AbilityExecution | null = null
+
+ async execute(
+ ability: Ability,
+ inputs: Record,
+ ctx: ExecutorContext
+ ): Promise {
+ // Block concurrent executions
+ if (this.activeExecution && this.activeExecution.status === 'running') {
+ throw new Error(`Already executing ability: ${this.activeExecution.ability.name}`)
+ }
+
+ console.log(`[abilities] Starting execution: ${ability.name}`)
+
+ const execution = await executeAbility(ability, inputs, ctx)
+ this.activeExecution = execution
+
+ // Clear active if completed/failed
+ if (execution.status !== 'running') {
+ this.activeExecution = null
+ }
+
+ return execution
+ }
+
+ getActive(): AbilityExecution | null {
+ return this.activeExecution
+ }
+
+ cancel(): boolean {
+ if (!this.activeExecution) return false
+
+ if (this.activeExecution.status === 'running') {
+ this.activeExecution.status = 'failed'
+ this.activeExecution.error = 'Cancelled by user'
+ this.activeExecution.completedAt = Date.now()
+ this.activeExecution = null
+ return true
+ }
+
+ return false
+ }
+
+ cleanup(): void {
+ this.activeExecution = null
+ }
+}
diff --git a/packages/plugin-abilities/src/executor/index.ts b/packages/plugin-abilities/src/executor/index.ts
new file mode 100644
index 0000000..1d419ba
--- /dev/null
+++ b/packages/plugin-abilities/src/executor/index.ts
@@ -0,0 +1,241 @@
+import { spawn } from 'child_process'
+import type {
+ Ability,
+ Step,
+ ScriptStep,
+ AbilityExecution,
+ StepResult,
+ ExecutorContext,
+ InputValues,
+} from '../types/index.js'
+import { validateInputs } from '../validator/index.js'
+
+/**
+ * Minimal Executor - Script Steps Only
+ *
+ * Stripped down to prove core concept:
+ * - Execute shell commands sequentially
+ * - Track step results
+ * - Validate exit codes
+ *
+ * NO: agent steps, skill steps, approval, workflows, context passing
+ */
+
+function generateExecutionId(): string {
+ return `exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
+}
+
+function interpolateVariables(text: string, inputs: InputValues): string {
+ return text.replace(/\{\{inputs\.(\w+)\}\}/g, (match, name) => {
+ const value = inputs[name]
+ return value !== undefined ? String(value) : match
+ })
+}
+
+async function runScript(
+ command: string,
+ options: { cwd?: string; env?: Record }
+): Promise<{ stdout: string; stderr: string; exitCode: number }> {
+ return new Promise((resolve) => {
+ const proc = spawn('sh', ['-c', command], {
+ cwd: options.cwd || process.cwd(),
+ env: { ...process.env, ...options.env },
+ })
+
+ let stdout = ''
+ let stderr = ''
+
+ proc.stdout.on('data', (data) => {
+ stdout += data.toString()
+ })
+
+ proc.stderr.on('data', (data) => {
+ stderr += data.toString()
+ })
+
+ proc.on('close', (code) => {
+ resolve({ stdout, stderr, exitCode: code ?? 1 })
+ })
+
+ proc.on('error', (error) => {
+ resolve({ stdout, stderr: error.message, exitCode: 1 })
+ })
+ })
+}
+
+async function executeScriptStep(
+ step: ScriptStep,
+ execution: AbilityExecution,
+ ctx: ExecutorContext
+): Promise {
+ const startedAt = Date.now()
+
+ const command = interpolateVariables(step.run, execution.inputs)
+
+ console.log(`[abilities] Executing: ${command}`)
+
+ try {
+ const result = await runScript(command, {
+ cwd: step.cwd || ctx.cwd,
+ env: { ...ctx.env, ...step.env },
+ })
+
+ // Validate exit code if specified
+ let failed = false
+ let error: string | undefined
+
+ if (step.validation?.exit_code !== undefined && result.exitCode !== step.validation.exit_code) {
+ failed = true
+ error = `Exit code ${result.exitCode}, expected ${step.validation.exit_code}`
+ }
+
+ return {
+ stepId: step.id,
+ status: failed ? 'failed' : 'completed',
+ output: result.stdout || result.stderr,
+ error,
+ startedAt,
+ completedAt: Date.now(),
+ duration: Date.now() - startedAt,
+ }
+ } catch (err) {
+ return {
+ stepId: step.id,
+ status: 'failed',
+ error: err instanceof Error ? err.message : String(err),
+ startedAt,
+ completedAt: Date.now(),
+ duration: Date.now() - startedAt,
+ }
+ }
+}
+
+function buildExecutionOrder(steps: Step[]): Step[] {
+ const result: Step[] = []
+ const completed = new Set()
+ const remaining = [...steps]
+
+ while (remaining.length > 0) {
+ const next = remaining.find((step) => {
+ if (!step.needs || step.needs.length === 0) return true
+ return step.needs.every((dep) => completed.has(dep))
+ })
+
+ if (!next) {
+ console.error('[abilities] Unable to resolve step order - circular dependency?')
+ break
+ }
+
+ result.push(next)
+ completed.add(next.id)
+ remaining.splice(remaining.indexOf(next), 1)
+ }
+
+ return result
+}
+
+export async function executeAbility(
+ ability: Ability,
+ inputs: InputValues,
+ ctx: ExecutorContext
+): Promise {
+ // Validate inputs
+ const inputErrors = validateInputs(ability, inputs)
+ if (inputErrors.length > 0) {
+ return {
+ id: generateExecutionId(),
+ ability,
+ inputs,
+ status: 'failed',
+ currentStep: null,
+ currentStepIndex: -1,
+ completedSteps: [],
+ pendingSteps: ability.steps,
+ startedAt: Date.now(),
+ completedAt: Date.now(),
+ error: `Input validation failed: ${inputErrors.map((e) => e.message).join(', ')}`,
+ }
+ }
+
+ // Apply defaults
+ const resolvedInputs: InputValues = { ...inputs }
+ if (ability.inputs) {
+ for (const [name, def] of Object.entries(ability.inputs)) {
+ if (resolvedInputs[name] === undefined && def.default !== undefined) {
+ resolvedInputs[name] = def.default
+ }
+ }
+ }
+
+ // Build execution order based on dependencies
+ const orderedSteps = buildExecutionOrder(ability.steps)
+
+ const execution: AbilityExecution = {
+ id: generateExecutionId(),
+ ability,
+ inputs: resolvedInputs,
+ status: 'running',
+ currentStep: null,
+ currentStepIndex: -1,
+ completedSteps: [],
+ pendingSteps: [...orderedSteps],
+ startedAt: Date.now(),
+ }
+
+ // Execute steps sequentially
+ for (let i = 0; i < orderedSteps.length; i++) {
+ const step = orderedSteps[i]
+ execution.currentStep = step
+ execution.currentStepIndex = i
+
+ console.log(`[abilities] Step ${i + 1}/${orderedSteps.length}: ${step.id}`)
+
+ const result = await executeScriptStep(step as ScriptStep, execution, ctx)
+ execution.completedSteps.push(result)
+ execution.pendingSteps = execution.pendingSteps.filter((s) => s.id !== step.id)
+
+ if (result.status === 'failed') {
+ execution.status = 'failed'
+ execution.error = result.error
+ execution.completedAt = Date.now()
+ return execution
+ }
+ }
+
+ execution.status = 'completed'
+ execution.currentStep = null
+ execution.completedAt = Date.now()
+
+ return execution
+}
+
+export function formatExecutionResult(execution: AbilityExecution): string {
+ const lines: string[] = []
+
+ lines.push(`Ability: ${execution.ability.name}`)
+ lines.push(`Status: ${execution.status === 'completed' ? '✅ Complete' : '❌ Failed'}`)
+
+ if (execution.error) {
+ lines.push(`Error: ${execution.error}`)
+ }
+
+ lines.push('')
+ lines.push('Steps:')
+
+ for (const result of execution.completedSteps) {
+ const icon = result.status === 'completed' ? '✅' : '❌'
+ const duration = result.duration ? ` (${(result.duration / 1000).toFixed(1)}s)` : ''
+ lines.push(` ${icon} ${result.stepId}${duration}`)
+ if (result.error) {
+ lines.push(` Error: ${result.error}`)
+ }
+ }
+
+ const totalDuration = execution.completedAt
+ ? ((execution.completedAt - execution.startedAt) / 1000).toFixed(1)
+ : 'N/A'
+ lines.push('')
+ lines.push(`Duration: ${totalDuration}s`)
+
+ return lines.join('\n')
+}
diff --git a/packages/plugin-abilities/src/index.ts b/packages/plugin-abilities/src/index.ts
new file mode 100644
index 0000000..f5e8fb2
--- /dev/null
+++ b/packages/plugin-abilities/src/index.ts
@@ -0,0 +1,34 @@
+/**
+ * @openagents/plugin-abilities - Minimal Version
+ *
+ * Enforced, validated workflows for OpenCode agents.
+ * Stripped to essentials for testing core concept.
+ */
+
+// Core types
+export type {
+ Ability,
+ Step,
+ ScriptStep,
+ InputDefinition,
+ InputValues,
+ AbilityExecution,
+ StepResult,
+ ExecutorContext,
+ LoadedAbility,
+ ValidationResult,
+} from './types/index.js'
+
+// Loader
+export { loadAbilities, loadAbility } from './loader/index.js'
+
+// Validator
+export { validateAbility, validateInputs } from './validator/index.js'
+
+// Executor
+export { executeAbility, formatExecutionResult } from './executor/index.js'
+export { ExecutionManager } from './executor/execution-manager.js'
+
+// Plugin
+export { AbilitiesPlugin } from './opencode-plugin.js'
+export { default } from './opencode-plugin.js'
diff --git a/packages/plugin-abilities/src/loader/index.ts b/packages/plugin-abilities/src/loader/index.ts
new file mode 100644
index 0000000..1cdf548
--- /dev/null
+++ b/packages/plugin-abilities/src/loader/index.ts
@@ -0,0 +1,164 @@
+import * as fs from 'fs/promises'
+import * as path from 'path'
+import { glob } from 'glob'
+import { parse as parseYaml } from 'yaml'
+import type { Ability, LoadedAbility, LoaderOptions } from '../types/index.js'
+
+const ABILITY_FILENAME = 'ability.yaml'
+const ABILITY_PATTERNS = ['*.yaml', '*/ability.yaml', '*/*.yaml', '*/*/ability.yaml']
+
+const DEFAULT_PROJECT_DIR = '.opencode/abilities'
+const DEFAULT_GLOBAL_DIR = '~/.config/opencode/abilities'
+
+function expandHome(filePath: string): string {
+ if (filePath.startsWith('~/')) {
+ const home = process.env.HOME || process.env.USERPROFILE || ''
+ return path.join(home, filePath.slice(2))
+ }
+ return filePath
+}
+
+function resolveAbilityName(filePath: string, baseDir: string): string {
+ const relativePath = path.relative(baseDir, filePath)
+ const dir = path.dirname(relativePath)
+ const filename = path.basename(filePath, '.yaml')
+
+ if (filename === 'ability') {
+ return dir === '.' ? path.basename(path.dirname(filePath)) : dir
+ }
+
+ if (dir === '.') {
+ return filename
+ }
+
+ return `${dir}/${filename}`
+}
+
+async function fileExists(filePath: string): Promise {
+ try {
+ await fs.access(filePath)
+ return true
+ } catch {
+ return false
+ }
+}
+
+async function loadAbilityFile(
+ filePath: string,
+ baseDir: string,
+ source: 'project' | 'global'
+): Promise {
+ try {
+ const content = await fs.readFile(filePath, 'utf-8')
+ const parsed = parseYaml(content) as Partial
+
+ if (!parsed || typeof parsed !== 'object') {
+ console.warn(`[abilities] Invalid YAML in ${filePath}`)
+ return null
+ }
+
+ const resolvedName = parsed.name || resolveAbilityName(filePath, baseDir)
+
+ const ability: Ability = {
+ ...parsed,
+ name: resolvedName,
+ description: parsed.description || '',
+ steps: parsed.steps || [],
+ _meta: {
+ filePath,
+ directory: path.dirname(filePath),
+ loadedAt: Date.now(),
+ },
+ }
+
+ return { ability, filePath, source }
+ } catch (error) {
+ console.error(`[abilities] Failed to load ${filePath}:`, error)
+ return null
+ }
+}
+
+async function discoverAbilities(
+ baseDir: string,
+ source: 'project' | 'global'
+): Promise {
+ const expandedDir = expandHome(baseDir)
+
+ if (!(await fileExists(expandedDir))) {
+ return []
+ }
+
+ const files: string[] = []
+ for (const pattern of ABILITY_PATTERNS) {
+ const matches = await glob(path.join(expandedDir, pattern), { nodir: true })
+ files.push(...matches)
+ }
+
+ const uniqueFiles = [...new Set(files)]
+ const results: LoadedAbility[] = []
+
+ for (const file of uniqueFiles) {
+ const loaded = await loadAbilityFile(file, expandedDir, source)
+ if (loaded) {
+ results.push(loaded)
+ }
+ }
+
+ return results
+}
+
+export async function loadAbilities(
+ options: LoaderOptions = {}
+): Promise