Преглед изворни кода

Merge remote-tracking branch 'origin/main' into revert-install-changes

darrenhinde пре 6 месеци
родитељ
комит
f7a17f8a3a
62 измењених фајлова са 9355 додато и 120 уклоњено
  1. 1 1
      .opencode/agent/subagents/core/contextscout.md
  2. 445 0
      .opencode/context/openagents-repo/guides/adding-skill.md
  3. 260 0
      .opencode/skill/smart-router-skill/SKILL.md
  4. 47 0
      .opencode/skill/smart-router-skill/config/personality-config.json
  5. 127 0
      .opencode/skill/smart-router-skill/router.sh
  6. 132 0
      .opencode/skill/smart-router-skill/scripts/sherlock-workflow.sh
  7. 131 0
      .opencode/skill/smart-router-skill/scripts/stark-workflow.sh
  8. 124 0
      .opencode/skill/smart-router-skill/scripts/yoda-workflow.sh
  9. 203 0
      .opencode/skill/task-management/SKILL.md
  10. 33 0
      .opencode/skill/task-management/router.sh
  11. 437 0
      .opencode/skill/task-management/scripts/task-cli.ts
  12. 1 1
      evals/framework/scripts/debug/test-debug.sh
  13. 1 1
      evals/framework/scripts/utils/run-tests-batch.sh
  14. 1 1
      evals/results/serve.sh
  15. 123 38
      install.sh
  16. 855 0
      packages/plugin-abilities/ARCHITECTURE.md
  17. 180 0
      packages/plugin-abilities/MINIMAL_TEST.md
  18. 320 0
      packages/plugin-abilities/QUICK_REFERENCE.md
  19. 401 0
      packages/plugin-abilities/README.md
  20. 187 0
      packages/plugin-abilities/SIMPLIFICATION_SUMMARY.md
  21. 122 0
      packages/plugin-abilities/bun.lock
  22. 177 0
      packages/plugin-abilities/docs/GETTING_STARTED.md
  23. 72 0
      packages/plugin-abilities/examples/deploy/ability.yaml
  24. 44 0
      packages/plugin-abilities/examples/test-suite/ability.yaml
  25. 32 0
      packages/plugin-abilities/examples/test/ability.yaml
  26. 61 0
      packages/plugin-abilities/package.json
  27. 59 0
      packages/plugin-abilities/src/executor/execution-manager.ts
  28. 241 0
      packages/plugin-abilities/src/executor/index.ts
  29. 34 0
      packages/plugin-abilities/src/index.ts
  30. 164 0
      packages/plugin-abilities/src/loader/index.ts
  31. 227 0
      packages/plugin-abilities/src/opencode-plugin.ts
  32. 801 0
      packages/plugin-abilities/src/plugin.ts
  33. 259 0
      packages/plugin-abilities/src/sdk.ts
  34. 124 0
      packages/plugin-abilities/src/types/index.ts
  35. 356 0
      packages/plugin-abilities/src/validator/index.ts
  36. 82 0
      packages/plugin-abilities/test-minimal.ts
  37. 87 0
      packages/plugin-abilities/test-plugin.ts
  38. 46 0
      packages/plugin-abilities/test-run-ability.ts
  39. 311 0
      packages/plugin-abilities/tests/context-passing.test.ts
  40. 272 0
      packages/plugin-abilities/tests/enforcement.test.ts
  41. 471 0
      packages/plugin-abilities/tests/executor.test.ts
  42. 300 0
      packages/plugin-abilities/tests/integration.test.ts
  43. 131 0
      packages/plugin-abilities/tests/sdk.test.ts
  44. 131 0
      packages/plugin-abilities/tests/trigger.test.ts
  45. 174 0
      packages/plugin-abilities/tests/validator.test.ts
  46. 23 0
      packages/plugin-abilities/tsconfig.json
  47. 347 1
      registry.json
  48. 10 5
      scripts/auto-detect-components.sh
  49. 8 4
      scripts/check-context-logs/count-agent-tokens.sh
  50. 2 2
      scripts/check-context-logs/show-api-payload.sh
  51. 2 2
      scripts/development/demo.sh
  52. 2 1
      scripts/prompts/test-prompt.sh
  53. 2 2
      scripts/prompts/use-prompt.sh
  54. 47 24
      scripts/registry/auto-detect-components.sh
  55. 2 1
      scripts/registry/validate-component.sh
  56. 0 1
      scripts/registry/validate-profile-coverage.sh
  57. 61 17
      scripts/registry/validate-registry.sh
  58. 33 2
      scripts/testing/test.sh
  59. 2 1
      scripts/tests/test-collision-detection.sh
  60. 16 9
      scripts/validate-registry.sh
  61. 10 5
      scripts/validation/validate-test-suites.sh
  62. 1 1
      update.sh

+ 1 - 1
.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:

+ 445 - 0
.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 <command> [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 <command> [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 <command> [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 <feature> <seq> "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 <command>"
+        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`

+ 260 - 0
.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 <your_choice>
+```
+
+### 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! 🚀

+ 47 - 0
.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"
+      }
+    }
+  }
+}

+ 127 - 0
.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 <name>    Character to embody (yoda, stark, sherlock)
+  -m, --mission <number>    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"

+ 132 - 0
.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

+ 131 - 0
.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

+ 124 - 0
.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

+ 203 - 0
.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 <feature> <seq> "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 <feature> <seq>` | Show dependency tree for a specific subtask |
+| `blocked [feature]` | Show blocked tasks and why |
+| `complete <feature> <seq> "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!

+ 33 - 0
.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" "$@"

+ 437 - 0
.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 <command> [feature] [args...]
+ *
+ * Commands:
+ *   status [feature]              - Show task status summary
+ *   next [feature]                - Show next eligible tasks
+ *   parallel [feature]            - Show parallelizable tasks ready to run
+ *   deps <feature> <seq>          - Show dependency tree for a task
+ *   blocked [feature]             - Show blocked tasks and why
+ *   complete <feature> <seq> "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<string>();
+      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 <feature> <seq>');
+      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 <feature> <seq> "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 <command> [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 <feature> <seq>              Show dependency tree for a task
+  blocked [feature]                 Show blocked tasks and why
+  complete <feature> <seq> "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"
+`);
+}

+ 1 - 1
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

+ 1 - 1
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 "----------------------------------------"
     

+ 1 - 1
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..."

+ 123 - 38
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

+ 855 - 0
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<string, LoadedAbility> {
+  '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<string, InputDefinition>
+  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<string, string>   // 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 <name>` 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.

+ 180 - 0
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.

+ 320 - 0
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.

+ 401 - 0
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 <name>` - Validate an ability
+- `ability.run <name> [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

+ 187 - 0
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.

+ 122 - 0
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=="],
+  }
+}

+ 177 - 0
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

+ 72 - 0
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

+ 44 - 0
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

+ 32 - 0
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

+ 61 - 0
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"
+  }
+}

+ 59 - 0
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<string, unknown>,
+    ctx: ExecutorContext
+  ): Promise<AbilityExecution> {
+    // 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
+  }
+}

+ 241 - 0
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<string, string> }
+): 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<StepResult> {
+  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<string>()
+  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<AbilityExecution> {
+  // 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')
+}

+ 34 - 0
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'

+ 164 - 0
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<boolean> {
+  try {
+    await fs.access(filePath)
+    return true
+  } catch {
+    return false
+  }
+}
+
+async function loadAbilityFile(
+  filePath: string,
+  baseDir: string,
+  source: 'project' | 'global'
+): Promise<LoadedAbility | null> {
+  try {
+    const content = await fs.readFile(filePath, 'utf-8')
+    const parsed = parseYaml(content) as Partial<Ability>
+
+    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<LoadedAbility[]> {
+  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<Map<string, LoadedAbility>> {
+  const {
+    projectDir = DEFAULT_PROJECT_DIR,
+    globalDir = DEFAULT_GLOBAL_DIR,
+    includeGlobal = true,
+  } = options
+
+  const abilities = new Map<string, LoadedAbility>()
+
+  if (includeGlobal) {
+    const globalAbilities = await discoverAbilities(globalDir, 'global')
+    for (const loaded of globalAbilities) {
+      abilities.set(loaded.ability.name, loaded)
+    }
+  }
+
+  const projectAbilities = await discoverAbilities(projectDir, 'project')
+  for (const loaded of projectAbilities) {
+    abilities.set(loaded.ability.name, loaded)
+  }
+
+  return abilities
+}
+
+export async function loadAbility(
+  name: string,
+  options: LoaderOptions = {}
+): Promise<LoadedAbility | null> {
+  const abilities = await loadAbilities(options)
+  return abilities.get(name) || null
+}
+
+export interface AbilityListItem {
+  name: string
+  description: string
+  source: 'project' | 'global'
+  triggers?: string[]
+  inputCount: number
+  stepCount: number
+}
+
+export function listAbilities(
+  abilities: Map<string, LoadedAbility>
+): AbilityListItem[] {
+  return Array.from(abilities.values()).map(({ ability, source }) => ({
+    name: ability.name,
+    description: ability.description,
+    source,
+    triggers: ability.triggers?.keywords,
+    inputCount: ability.inputs ? Object.keys(ability.inputs).length : 0,
+    stepCount: ability.steps.length,
+  }))
+}

+ 227 - 0
packages/plugin-abilities/src/opencode-plugin.ts

@@ -0,0 +1,227 @@
+import type { Plugin } from '@opencode-ai/plugin'
+import { tool } from '@opencode-ai/plugin'
+import type { Ability, LoadedAbility, ExecutorContext } from './types/index.js'
+import { loadAbilities } from './loader/index.js'
+import { validateAbility, validateInputs } from './validator/index.js'
+import { formatExecutionResult } from './executor/index.js'
+import { ExecutionManager } from './executor/execution-manager.js'
+
+/**
+ * Minimal Abilities Plugin
+ * 
+ * Stripped to essentials:
+ * - Load abilities from YAML
+ * - Execute script steps sequentially
+ * - Block tools during execution (enforcement)
+ * - Inject context into chat messages
+ * 
+ * NO: sessions, agents, triggers, toasts, cleanup timers
+ */
+
+// Tools that are ALWAYS allowed (read-only)
+const ALWAYS_ALLOWED_TOOLS = [
+  'ability.list',
+  'ability.status',
+  'ability.cancel',
+  'read',
+  'glob',
+  'grep',
+]
+
+export const AbilitiesPlugin: Plugin = async (ctx) => {
+  const abilities = new Map<string, LoadedAbility>()
+  const executionManager = new ExecutionManager()
+
+  const abilitiesDir = `${ctx.directory}/.opencode/abilities`
+
+  // Load abilities on startup
+  try {
+    const loaded = await loadAbilities({ projectDir: abilitiesDir, includeGlobal: false })
+    for (const [name, ability] of loaded) {
+      abilities.set(name, ability)
+    }
+    console.log(`[abilities] Loaded ${abilities.size} abilities from ${abilitiesDir}`)
+  } catch (err) {
+    console.log(`[abilities] Could not load abilities:`, err instanceof Error ? err.message : err)
+  }
+
+  const createExecutorContext = (): ExecutorContext => {
+    return {
+      cwd: ctx.directory,
+      env: {},
+    }
+  }
+
+  const buildAbilityContextInjection = (execution: any): string => {
+    const ability = execution.ability
+    const currentStep = execution.currentStep
+    const completed = execution.completedSteps.length
+    const total = ability.steps.length
+
+    const lines = [
+      `## 🔄 Active Ability: ${ability.name}`,
+      '',
+      `**Progress:** ${completed}/${total} steps completed`,
+      '',
+    ]
+
+    if (currentStep) {
+      lines.push(`### Current Step: ${currentStep.id}`)
+      if (currentStep.description) lines.push(currentStep.description)
+      lines.push('')
+      lines.push(`**Action:** Script is executing. Wait for completion.`)
+      lines.push('')
+      lines.push('⚠️ **ENFORCEMENT ACTIVE** - Other tools are blocked until step completes.')
+    }
+
+    return lines.join('\n')
+  }
+
+  return {
+    // Hook: Inject ability context into every chat message
+    async 'chat.message'(_input, output) {
+      try {
+        const activeExecution = executionManager.getActive()
+
+        if (activeExecution && activeExecution.status === 'running') {
+          output.parts.unshift({
+            type: 'text',
+            text: buildAbilityContextInjection(activeExecution),
+          } as any)
+        }
+      } catch (err) {
+        console.error('[abilities] chat.message error:', err)
+      }
+    },
+
+    // Hook: Block unauthorized tools during ability execution
+    async 'tool.execute.before'(input, _output) {
+      try {
+        const execution = executionManager.getActive()
+        if (!execution) return // No ability running, allow all tools
+
+        const currentStep = execution.currentStep
+        if (!currentStep) return
+
+        // Always allow read-only tools
+        if (ALWAYS_ALLOWED_TOOLS.includes(input.tool)) return
+
+        // Script steps block ALL other tools (deterministic execution)
+        if (currentStep.type === 'script') {
+          throw new Error(
+            `[abilities] Tool '${input.tool}' blocked during script step '${currentStep.id}'. ` +
+            `Script steps run deterministically - wait for completion.`
+          )
+        }
+      } catch (err) {
+        if (err instanceof Error && err.message.startsWith('[abilities]')) {
+          throw err
+        }
+        console.error('[abilities] tool.execute.before error:', err)
+      }
+    },
+
+    // Hook: Cleanup on session deletion
+    async event({ event }) {
+      try {
+        if (event.type === 'session.deleted') {
+          executionManager.cleanup()
+        }
+      } catch (err) {
+        console.error('[abilities] event handler error:', err)
+      }
+    },
+
+    tool: {
+      'ability.list': tool({
+        description: 'List all available abilities',
+        args: {},
+        async execute() {
+          if (abilities.size === 0) return 'No abilities loaded.'
+          
+          const list = Array.from(abilities.values()).map(loaded => {
+            const stepCount = loaded.ability.steps.length
+            return `- **${loaded.ability.name}**: ${loaded.ability.description} (${stepCount} steps)`
+          })
+          
+          return list.join('\n')
+        },
+      }),
+
+      'ability.run': tool({
+        description: `Execute an ability workflow. Available: ${Array.from(abilities.keys()).join(', ') || 'none loaded'}`,
+        args: {
+          name: tool.schema.string().describe('Ability name to run'),
+          inputs: tool.schema.optional(tool.schema.any()).describe('Input values for the ability'),
+        },
+        async execute({ name, inputs = {} }) {
+          const loaded = abilities.get(name)
+          if (!loaded) {
+            return JSON.stringify({ error: `Ability '${name}' not found` })
+          }
+
+          const ability = loaded.ability
+          
+          // Validate inputs
+          const inputErrors = validateInputs(ability, inputs as Record<string, unknown>)
+          if (inputErrors.length > 0) {
+            return JSON.stringify({ 
+              error: 'Input validation failed', 
+              details: inputErrors.map(e => e.message) 
+            })
+          }
+
+          try {
+            const execution = await executionManager.execute(
+              ability, 
+              inputs as Record<string, unknown>, 
+              createExecutorContext()
+            )
+            
+            return JSON.stringify({
+              status: execution.status,
+              ability: ability.name,
+              result: formatExecutionResult(execution),
+            })
+          } catch (error) {
+            return JSON.stringify({ 
+              status: 'error', 
+              error: error instanceof Error ? error.message : String(error) 
+            })
+          }
+        },
+      }),
+
+      'ability.status': tool({
+        description: 'Get status of active ability execution',
+        args: {},
+        async execute() {
+          const execution = executionManager.getActive()
+          if (!execution) {
+            return JSON.stringify({ status: 'none', message: 'No active ability' })
+          }
+
+          return JSON.stringify({
+            status: execution.status,
+            ability: execution.ability.name,
+            currentStep: execution.currentStep?.id,
+            progress: `${execution.completedSteps.length}/${execution.ability.steps.length}`,
+          })
+        },
+      }),
+
+      'ability.cancel': tool({
+        description: 'Cancel the active ability execution',
+        args: {},
+        async execute() {
+          const cancelled = executionManager.cancel()
+          return JSON.stringify(cancelled
+            ? { status: 'cancelled', message: 'Ability cancelled' }
+            : { status: 'none', message: 'No active ability' })
+        },
+      }),
+    },
+  }
+}
+
+export default AbilitiesPlugin

+ 801 - 0
packages/plugin-abilities/src/plugin.ts

@@ -0,0 +1,801 @@
+import type { Ability, LoadedAbility, ExecutorContext, AbilityExecution, Step, AgentStep, SkillStep, ApprovalStep, WorkflowStep } from './types/index.js'
+import { loadAbilities, listAbilities } from './loader/index.js'
+import { validateAbility, validateInputs } from './validator/index.js'
+import { executeAbility, formatExecutionResult } from './executor/index.js'
+import { ExecutionManager } from './executor/execution-manager.js'
+
+interface OpencodeClient {
+  session: {
+    get: (options: { path: { id: string } }) => Promise<unknown>
+    list: () => Promise<Array<{ id: string }>>
+    command: (options: { path: { id: string }; body: { command: string; arguments: string; agent?: string; model?: string } }) => Promise<unknown>
+    prompt: (options: unknown) => Promise<unknown>
+    todo: (options: unknown) => Promise<unknown>
+  }
+  events: {
+    publish: (options: { body: { type: string; properties: Record<string, unknown> } }) => Promise<void>
+  }
+}
+
+interface PluginContext {
+  directory: string
+  worktree: string
+  client: OpencodeClient
+  $: (strings: TemplateStringsArray, ...values: unknown[]) => { text: () => Promise<string> }
+}
+
+interface PluginConfig {
+  abilities?: {
+    enabled?: boolean
+    auto_trigger?: boolean
+    enforcement?: 'strict' | 'normal' | 'loose'
+    directories?: string[]
+  }
+  disabled_abilities?: string[]
+}
+
+interface EventInput {
+  event: {
+    type: string
+    properties?: Record<string, unknown>
+  }
+}
+
+interface ChatMessageOutput {
+  parts: Array<{ type: string; text: string; synthetic?: boolean }>
+}
+
+interface ToolExecuteInput {
+  tool: string
+  sessionID?: string
+  callID?: string
+}
+
+interface ToolExecuteOutput {
+  args: Record<string, unknown>
+}
+
+interface SessionIdleOutput {
+  inject?: string
+}
+
+// Tools allowed during different step types
+const ALLOWED_TOOLS_BY_STEP_TYPE: Record<string, string[]> = {
+  // Script steps block ALL tools (script runs deterministically)
+  script: [],
+  // Agent steps allow agent-invocation tools
+  agent: ['task', 'background_task', 'call_omo_agent'],
+  // Skill steps allow skill-loading tools  
+  skill: ['skill', 'slashcommand'],
+  // Approval steps only allow approval-related actions
+  approval: ['ability.status', 'ability.cancel'],
+  // Workflow steps allow nested ability execution
+  workflow: ['ability.run', 'ability.status'],
+}
+
+// Tools always allowed regardless of step (read-only/status tools)
+const ALWAYS_ALLOWED_TOOLS = [
+  'ability.list',
+  'ability.status',
+  'ability.cancel',
+  'todoread',
+  'read',
+  'glob',
+  'grep',
+  'lsp_hover',
+  'lsp_diagnostics',
+  'lsp_document_symbols',
+]
+
+class AbilitiesPlugin {
+  private abilities: Map<string, LoadedAbility> = new Map()
+  private executionManager: ExecutionManager
+  private config: PluginConfig = {}
+  private ctx: PluginContext | null = null
+  private mainSessionID: string | undefined
+  private initialized = false
+  private currentAgentId: string | undefined
+  private agentAbilityBindings: Map<string, string[]> = new Map()
+
+  constructor() {
+    this.executionManager = new ExecutionManager()
+  }
+
+  async initialize(ctx: PluginContext, config: PluginConfig = {}): Promise<void> {
+    this.ctx = ctx
+    this.config = config
+
+    const directories = config.abilities?.directories || [
+      `${ctx.directory}/.opencode/abilities`,
+    ]
+
+    for (const dir of directories) {
+      const loaded = await loadAbilities({ projectDir: dir, includeGlobal: true })
+      for (const [name, ability] of loaded) {
+        if (!config.disabled_abilities?.includes(name)) {
+          this.abilities.set(name, ability)
+        }
+      }
+    }
+
+    this.initialized = true
+    console.log(`[abilities] Loaded ${this.abilities.size} abilities`)
+  }
+
+  private matchesTrigger(text: string, ability: Ability): boolean {
+    if (!ability.triggers) return false
+    if (this.config.abilities?.auto_trigger === false) return false
+
+    const lowerText = text.toLowerCase()
+
+    if (ability.triggers.keywords) {
+      for (const keyword of ability.triggers.keywords) {
+        if (lowerText.includes(keyword.toLowerCase())) {
+          return true
+        }
+      }
+    }
+
+    if (ability.triggers.patterns) {
+      for (const pattern of ability.triggers.patterns) {
+        try {
+          if (new RegExp(pattern, 'i').test(text)) {
+            return true
+          }
+        } catch {
+          continue
+        }
+      }
+    }
+
+    return false
+  }
+
+  private detectAbility(text: string): Ability | null {
+    for (const [, loaded] of this.abilities) {
+      if (this.matchesTrigger(text, loaded.ability)) {
+        return loaded.ability
+      }
+    }
+    return null
+  }
+
+  private formatPlan(ability: Ability, inputs: Record<string, unknown>): string {
+    const lines: string[] = []
+
+    lines.push(`## Ability: ${ability.name}`)
+    lines.push(ability.description)
+    lines.push('')
+
+    if (Object.keys(inputs).length > 0) {
+      lines.push('### Inputs')
+      for (const [key, value] of Object.entries(inputs)) {
+        lines.push(`- ${key}: ${JSON.stringify(value)}`)
+      }
+      lines.push('')
+    }
+
+    lines.push('### Steps')
+    for (let i = 0; i < ability.steps.length; i++) {
+      const step = ability.steps[i]
+      const deps = step.needs?.length ? ` (after: ${step.needs.join(', ')})` : ''
+      lines.push(`${i + 1}. **${step.id}** [${step.type}]${deps}`)
+      if (step.description) {
+        lines.push(`   ${step.description}`)
+      }
+    }
+
+    return lines.join('\n')
+  }
+
+  private formatAbilityList(): string {
+    if (this.abilities.size === 0) {
+      return 'No abilities loaded.'
+    }
+
+    const list = listAbilities(this.abilities)
+    const lines: string[] = ['Available abilities:', '']
+
+    for (const item of list) {
+      lines.push(`- **${item.name}** (${item.source})`)
+      lines.push(`  ${item.description}`)
+    }
+
+    return lines.join('\n')
+  }
+
+  private async showToast(title: string, message: string, variant: 'success' | 'error' | 'info' = 'info'): Promise<void> {
+    if (!this.ctx?.client?.events?.publish) return
+
+    try {
+      await this.ctx.client.events.publish({
+        body: {
+          type: 'tui.toast.show',
+          properties: {
+            title,
+            message,
+            variant,
+            duration: 5000
+          }
+        }
+      })
+    } catch {
+      console.log(`[abilities] Toast: ${title} - ${message}`)
+    }
+  }
+
+  private createExecutorContext(): ExecutorContext {
+    const ctx = this.ctx
+    const self = this
+
+    return {
+      cwd: ctx?.directory || process.cwd(),
+      env: {},
+
+      agents: ctx?.client ? {
+        async call(options: { agent: string; prompt: string }): Promise<string> {
+          console.log(`[abilities] Agent step: ${options.agent}`)
+          console.log(`[abilities] Prompt: ${options.prompt.slice(0, 200)}...`)
+
+          return `[Agent "${options.agent}" invocation pending - use Task tool with subagent_type="${options.agent}" and the provided prompt]`
+        },
+        async background(options: { agent: string; prompt: string }): Promise<string> {
+          return this.call(options)
+        }
+      } : undefined,
+
+      skills: ctx?.client ? {
+        async load(name: string): Promise<string> {
+          console.log(`[abilities] Skill step: ${name}`)
+          return `[Skill "${name}" loaded - follow skill instructions]`
+        }
+      } : undefined,
+
+      approval: ctx?.client ? {
+        async request(options: { prompt: string; options?: string[] }): Promise<boolean> {
+          console.log(`[abilities] Approval required: ${options.prompt}`)
+          await self.showToast('Approval Required', options.prompt, 'info')
+          return true
+        }
+      } : undefined,
+
+      abilities: {
+        get: (name: string) => {
+          const loaded = self.abilities.get(name)
+          return loaded?.ability
+        },
+        execute: async (ability, inputs) => {
+          console.log(`[abilities] Nested workflow: ${ability.name}`)
+          return executeAbility(ability, inputs, self.createExecutorContext())
+        }
+      },
+
+      onStepStart: (step) => {
+        console.log(`[abilities] Step started: ${step.id} (${step.type})`)
+      },
+      onStepComplete: (step, result) => {
+        console.log(`[abilities] Step completed: ${step.id} - ${result.status}`)
+      },
+      onStepFail: (step, error) => {
+        console.log(`[abilities] Step failed: ${step.id} - ${error.message}`)
+      }
+    }
+  }
+
+  async handleEvent(input: EventInput): Promise<void> {
+    const { event } = input
+    const props = event.properties
+
+    if (event.type === 'session.created') {
+      const sessionInfo = props?.info as { id?: string; parentID?: string } | undefined
+      if (!sessionInfo?.parentID) {
+        this.mainSessionID = sessionInfo?.id
+      }
+    }
+
+    if (event.type === 'session.deleted') {
+      const sessionInfo = props?.info as { id?: string } | undefined
+      if (sessionInfo?.id) {
+        this.executionManager.onSessionDeleted(sessionInfo.id)
+
+        if (sessionInfo.id === this.mainSessionID) {
+          this.mainSessionID = undefined
+        }
+      }
+    }
+
+    if (event.type === 'agent.changed') {
+      const agentInfo = props?.agent as { id?: string; abilities?: string[] } | undefined
+      if (agentInfo?.id) {
+        this.setCurrentAgent(agentInfo.id)
+        if (agentInfo.abilities && agentInfo.abilities.length > 0) {
+          this.registerAgentAbilities(agentInfo.id, agentInfo.abilities)
+        }
+      }
+    }
+  }
+
+  async handleSessionIdle(): Promise<SessionIdleOutput> {
+    const execution = this.executionManager.getActive()
+    
+    if (!execution || execution.status !== 'running') {
+      return {}
+    }
+
+    const currentStep = execution.currentStep
+    const enforcement = this.config.abilities?.enforcement || 'strict'
+
+    const lines: string[] = [
+      `## Ability In Progress: ${execution.ability.name}`,
+      '',
+      `Progress: ${execution.completedSteps.length}/${execution.ability.steps.length} steps`,
+    ]
+
+    if (currentStep) {
+      lines.push(`Current step: **${currentStep.id}** [${currentStep.type}]`)
+      lines.push('')
+      lines.push(this.getStepInstructions(currentStep))
+    }
+
+    if (enforcement === 'strict') {
+      lines.push('')
+      lines.push('**[STRICT]** You cannot exit or work on other tasks until this ability completes.')
+      lines.push('Use `ability.cancel` if you need to abort.')
+    } else {
+      lines.push('')
+      lines.push('Continue with the current step or use `ability.cancel` to abort.')
+    }
+
+    console.log(`[abilities] Session idle - injecting continuation for: ${execution.ability.name}`)
+
+    return {
+      inject: lines.join('\n')
+    }
+  }
+
+  async handleChatMessage(
+    _input: Record<string, unknown>,
+    output: ChatMessageOutput
+  ): Promise<void> {
+    const activeExecution = this.executionManager.getActive()
+    
+    if (activeExecution && activeExecution.status === 'running') {
+      const contextText = this.buildAbilityContextInjection(activeExecution)
+      output.parts.unshift({
+        type: 'text',
+        synthetic: true,
+        text: contextText,
+      })
+      return
+    }
+
+    const userText = output.parts
+      .filter((p) => p.type === 'text' && !p.synthetic)
+      .map((p) => p.text)
+      .join(' ')
+
+    const detected = this.detectAbility(userText)
+    if (detected) {
+      output.parts.unshift({
+        type: 'text',
+        synthetic: true,
+        text: `## Ability Detected: ${detected.name}\n\n${detected.description}\n\nUse \`ability.run\` tool with name="${detected.name}" to execute.`,
+      })
+    }
+  }
+
+  private buildAbilityContextInjection(execution: AbilityExecution): string {
+    const ability = execution.ability
+    const currentStep = execution.currentStep
+    const completed = execution.completedSteps.length
+    const total = ability.steps.length
+    const progress = `${completed}/${total}`
+
+    const lines: string[] = [
+      `## Active Ability: ${ability.name}`,
+      '',
+      `**Progress:** ${progress} steps completed`,
+      '',
+    ]
+
+    if (currentStep) {
+      lines.push(`### Current Step: ${currentStep.id} [${currentStep.type}]`)
+      if (currentStep.description) {
+        lines.push(currentStep.description)
+      }
+      lines.push('')
+      lines.push(this.getStepInstructions(currentStep))
+    }
+
+    const enforcement = this.config.abilities?.enforcement || 'strict'
+    if (enforcement === 'strict') {
+      lines.push('')
+      lines.push('**[STRICT MODE]** You MUST complete this step before proceeding. Other tools are blocked.')
+    }
+
+    return lines.join('\n')
+  }
+
+  private getStepInstructions(step: Step): string {
+    switch (step.type) {
+      case 'script':
+        return `**Action:** Script is executing. Wait for completion.`
+      case 'agent': {
+        const agentStep = step as AgentStep
+        return `**Action:** Invoke agent "${agentStep.agent}" with the prompt:\n\`\`\`\n${agentStep.prompt}\n\`\`\``
+      }
+      case 'skill': {
+        const skillStep = step as SkillStep
+        return `**Action:** Load and follow skill "${skillStep.skill}".`
+      }
+      case 'approval': {
+        const approvalStep = step as ApprovalStep
+        return `**Action:** Request user approval:\n"${approvalStep.prompt}"`
+      }
+      case 'workflow': {
+        const workflowStep = step as WorkflowStep
+        return `**Action:** Execute nested ability "${workflowStep.workflow}".`
+      }
+      default:
+        return '**Action:** Complete the current step.'
+    }
+  }
+
+  async handleToolExecuteBefore(
+    input: ToolExecuteInput,
+    _output: ToolExecuteOutput
+  ): Promise<void> {
+    const execution = this.executionManager.getActive()
+    if (!execution) return
+
+    const currentStep = execution.currentStep
+    if (!currentStep) return
+
+    const enforcement = this.config.abilities?.enforcement || 'strict'
+    if (enforcement === 'loose') return
+
+    const tool = input.tool
+
+    if (ALWAYS_ALLOWED_TOOLS.includes(tool)) {
+      return
+    }
+
+    const allowedTools = ALLOWED_TOOLS_BY_STEP_TYPE[currentStep.type] || []
+
+    if (enforcement === 'strict') {
+      if (!allowedTools.includes(tool)) {
+        const stepTypeMsg = this.getStepTypeBlockMessage(currentStep)
+        throw new Error(
+          `[abilities] Tool '${tool}' blocked during ${currentStep.type} step '${currentStep.id}'. ${stepTypeMsg}`
+        )
+      }
+    } else if (enforcement === 'normal') {
+      const destructiveTools = ['write', 'edit', 'bash', 'task']
+      if (destructiveTools.includes(tool) && !allowedTools.includes(tool)) {
+        throw new Error(
+          `[abilities] Destructive tool '${tool}' blocked during ${currentStep.type} step '${currentStep.id}'.`
+        )
+      }
+    }
+
+    console.log(`[abilities] Tool '${tool}' allowed during ${currentStep.type} step '${currentStep.id}'`)
+  }
+
+  private getStepTypeBlockMessage(step: Step): string {
+    switch (step.type) {
+      case 'script':
+        return 'Script steps run deterministically - wait for completion.'
+      case 'agent':
+        return 'Only agent invocation tools (task, background_task) allowed.'
+      case 'approval':
+        return 'Waiting for user approval - only status/cancel tools allowed.'
+      case 'skill':
+        return 'Only skill-loading tools allowed.'
+      case 'workflow':
+        return 'Only ability execution tools allowed.'
+      default:
+        return 'Wait for step completion.'
+    }
+  }
+
+  async handleToolExecuteAfter(
+    input: ToolExecuteInput,
+    output: ToolExecuteOutput
+  ): Promise<void> {
+    const execution = this.executionManager.getActive()
+    if (!execution) return
+
+    if (input.tool === 'ability.run') {
+      const result = output.args as { status?: string; ability?: string }
+      if (result.status === 'completed') {
+        this.showToast(
+          'Ability Complete',
+          `${result.ability} finished successfully`,
+          'success'
+        )
+      } else if (result.status === 'failed') {
+        this.showToast(
+          'Ability Failed',
+          `${result.ability} encountered an error`,
+          'error'
+        )
+      }
+    }
+  }
+
+  getTools() {
+    return {
+      'ability.list': {
+        description: 'List all available abilities',
+        parameters: {},
+        execute: async () => {
+          return this.formatAbilityList()
+        },
+      },
+
+      'ability.validate': {
+        description: 'Validate an ability definition',
+        parameters: {
+          name: { type: 'string', description: 'Ability name to validate' },
+        },
+        execute: async (params: { name: string }) => {
+          const loaded = this.abilities.get(params.name)
+          if (!loaded) {
+            return `Ability '${params.name}' not found`
+          }
+
+          const result = validateAbility(loaded.ability)
+          if (result.valid) {
+            return `Ability '${params.name}' is valid`
+          }
+
+          const errors = result.errors.map((e) => `- ${e.path}: ${e.message}`).join('\n')
+          return `Ability '${params.name}' has errors:\n${errors}`
+        },
+      },
+
+      'ability.run': {
+        description: `Execute an ability workflow.
+
+Available abilities:
+${Array.from(this.abilities.values())
+  .map((l) => `- ${l.ability.name}: ${l.ability.description}`)
+  .join('\n')}
+
+Use: ability.run({ name: "ability-name", inputs: { ... } })`,
+        parameters: {
+          name: { type: 'string', description: 'Ability name to run' },
+          inputs: { type: 'object', description: 'Input values for the ability' },
+        },
+        execute: async (
+          params: { name: string; inputs?: Record<string, unknown> }
+        ) => {
+          const loaded = this.abilities.get(params.name)
+          if (!loaded) {
+            return { error: `Ability '${params.name}' not found` }
+          }
+
+          const ability = loaded.ability
+
+          if (this.currentAgentId && !this.isAbilityAllowedForAgent(params.name, this.currentAgentId)) {
+            return { error: `Ability '${params.name}' is not allowed for agent '${this.currentAgentId}'` }
+          }
+
+          const inputs = params.inputs || {}
+
+          const inputErrors = validateInputs(ability, inputs)
+          if (inputErrors.length > 0) {
+            return {
+              error: 'Input validation failed',
+              details: inputErrors.map((e) => e.message),
+            }
+          }
+
+          const plan = this.formatPlan(ability, inputs)
+          console.log(`[abilities] Executing:\n${plan}`)
+
+          const executorCtx = this.createExecutorContext()
+
+          try {
+            const execution = await this.executionManager.execute(
+              ability,
+              inputs,
+              executorCtx
+            )
+
+            return {
+              status: execution.status,
+              ability: ability.name,
+              result: formatExecutionResult(execution),
+              steps: execution.completedSteps.map((s) => ({
+                id: s.stepId,
+                status: s.status,
+                duration: s.duration,
+              })),
+            }
+          } catch (error) {
+            return {
+              status: 'error',
+              error: error instanceof Error ? error.message : String(error),
+            }
+          }
+        },
+      },
+
+      'ability.status': {
+        description: 'Get status of active ability execution',
+        parameters: {},
+        execute: async () => {
+          const execution = this.executionManager.getActive()
+          if (!execution) {
+            return { status: 'none', message: 'No active ability execution' }
+          }
+
+          return {
+            status: execution.status,
+            ability: execution.ability.name,
+            currentStep: execution.currentStep?.id,
+            progress: `${execution.completedSteps.length}/${execution.ability.steps.length}`,
+            result: formatExecutionResult(execution),
+          }
+        },
+      },
+
+      'ability.cancel': {
+        description: 'Cancel the active ability execution',
+        parameters: {},
+        execute: async () => {
+          const cancelled = this.executionManager.cancelActive()
+          if (cancelled) {
+            return { status: 'cancelled', message: 'Ability execution cancelled' }
+          }
+          return { status: 'none', message: 'No active ability to cancel' }
+        },
+      },
+
+      'ability.agent': {
+        description: 'List abilities available to the current agent',
+        parameters: {
+          agent: { type: 'string', description: 'Agent ID (optional, defaults to current agent)' },
+        },
+        execute: async (params: { agent?: string }) => {
+          const agentId = params.agent || this.currentAgentId
+
+          if (!agentId) {
+            return {
+              message: 'No agent specified and no current agent set.',
+              hint: 'Provide an agent ID or use this tool after an agent is active.'
+            }
+          }
+
+          const abilities = this.getAgentAbilities(agentId)
+
+          if (abilities.length === 0) {
+            return {
+              agent: agentId,
+              abilities: [],
+              message: `No abilities registered for agent '${agentId}'`
+            }
+          }
+
+          return {
+            agent: agentId,
+            abilities: abilities.map(a => ({
+              name: a.ability.name,
+              description: a.ability.description,
+              triggers: a.ability.triggers?.keywords || [],
+              exclusive: a.ability.exclusive_agent === agentId,
+            })),
+          }
+        },
+      },
+    }
+  }
+
+  cleanup(): void {
+    this.executionManager.cleanup()
+    this.abilities.clear()
+    this.agentAbilityBindings.clear()
+    this.currentAgentId = undefined
+    this.initialized = false
+  }
+
+  registerAgentAbilities(agentId: string, abilityNames: string[]): void {
+    this.agentAbilityBindings.set(agentId, abilityNames)
+    console.log(`[abilities] Registered ${abilityNames.length} abilities for agent: ${agentId}`)
+  }
+
+  setCurrentAgent(agentId: string | undefined): void {
+    this.currentAgentId = agentId
+    if (agentId) {
+      console.log(`[abilities] Current agent set to: ${agentId}`)
+    }
+  }
+
+  getAgentAbilities(agentId: string): LoadedAbility[] {
+    const boundNames = this.agentAbilityBindings.get(agentId) || []
+    const result: LoadedAbility[] = []
+
+    for (const name of boundNames) {
+      const loaded = this.abilities.get(name)
+      if (loaded) {
+        result.push(loaded)
+      }
+    }
+
+    for (const [, loaded] of this.abilities) {
+      const ability = loaded.ability
+      if (ability.compatible_agents?.includes(agentId)) {
+        if (!result.find(r => r.ability.name === ability.name)) {
+          result.push(loaded)
+        }
+      }
+      if (ability.exclusive_agent === agentId) {
+        if (!result.find(r => r.ability.name === ability.name)) {
+          result.push(loaded)
+        }
+      }
+    }
+
+    return result
+  }
+
+  isAbilityAllowedForAgent(abilityName: string, agentId: string): boolean {
+    const loaded = this.abilities.get(abilityName)
+    if (!loaded) return false
+
+    const ability = loaded.ability
+
+    if (ability.exclusive_agent && ability.exclusive_agent !== agentId) {
+      return false
+    }
+
+    if (ability.compatible_agents && ability.compatible_agents.length > 0) {
+      return ability.compatible_agents.includes(agentId)
+    }
+
+    const boundNames = this.agentAbilityBindings.get(agentId)
+    if (boundNames) {
+      return boundNames.includes(abilityName)
+    }
+
+    return true
+  }
+
+  getCurrentAgent(): string | undefined {
+    return this.currentAgentId
+  }
+}
+
+const pluginInstance = new AbilitiesPlugin()
+
+export async function createAbilitiesPlugin(ctx: PluginContext, config: PluginConfig = {}) {
+  await pluginInstance.initialize(ctx, config)
+
+  return {
+    tool: pluginInstance.getTools(),
+
+    event: async (input: EventInput) => {
+      await pluginInstance.handleEvent(input)
+    },
+
+    'chat.message': async (input: Record<string, unknown>, output: ChatMessageOutput) => {
+      await pluginInstance.handleChatMessage(input, output)
+    },
+
+    'tool.execute.before': async (input: ToolExecuteInput, output: ToolExecuteOutput) => {
+      await pluginInstance.handleToolExecuteBefore(input, output)
+    },
+
+    'tool.execute.after': async (input: ToolExecuteInput, output: ToolExecuteOutput) => {
+      await pluginInstance.handleToolExecuteAfter(input, output)
+    },
+
+    'session.idle': async (): Promise<SessionIdleOutput> => {
+      return pluginInstance.handleSessionIdle()
+    },
+  }
+}
+
+export { AbilitiesPlugin }
+export default createAbilitiesPlugin

+ 259 - 0
packages/plugin-abilities/src/sdk.ts

@@ -0,0 +1,259 @@
+import type { Ability, AbilityExecution, ExecutorContext, LoadedAbility, InputValues } from './types/index.js'
+import { loadAbilities, listAbilities } from './loader/index.js'
+import { validateAbility, validateInputs } from './validator/index.js'
+import { executeAbility, formatExecutionResult } from './executor/index.js'
+import { ExecutionManager } from './executor/execution-manager.js'
+
+export interface AbilitiesSDKOptions {
+  projectDir?: string
+  globalDir?: string
+  includeGlobal?: boolean
+}
+
+export interface AbilityInfo {
+  name: string
+  description: string
+  source: 'project' | 'global'
+  triggers?: string[]
+  inputCount: number
+  stepCount: number
+}
+
+export interface ExecutionResult {
+  id: string
+  status: 'completed' | 'failed' | 'cancelled'
+  ability: string
+  duration: number
+  steps: Array<{
+    id: string
+    status: string
+    duration?: number
+    output?: string
+    error?: string
+  }>
+  error?: string
+  formatted: string
+}
+
+export class AbilitiesSDK {
+  private abilities: Map<string, LoadedAbility> = new Map()
+  private executionManager: ExecutionManager
+  private initialized = false
+  private options: AbilitiesSDKOptions
+
+  constructor(options: AbilitiesSDKOptions = {}) {
+    this.options = options
+    this.executionManager = new ExecutionManager()
+  }
+
+  async initialize(): Promise<void> {
+    if (this.initialized) return
+
+    const loaded = await loadAbilities({
+      projectDir: this.options.projectDir,
+      globalDir: this.options.globalDir,
+      includeGlobal: this.options.includeGlobal ?? true,
+    })
+
+    for (const [name, ability] of loaded) {
+      this.abilities.set(name, ability)
+    }
+
+    this.initialized = true
+  }
+
+  async list(): Promise<AbilityInfo[]> {
+    await this.initialize()
+
+    return listAbilities(this.abilities).map(item => ({
+      name: item.name,
+      description: item.description,
+      source: item.source,
+      triggers: item.triggers,
+      inputCount: item.inputCount,
+      stepCount: item.stepCount,
+    }))
+  }
+
+  async get(name: string): Promise<Ability | undefined> {
+    await this.initialize()
+    return this.abilities.get(name)?.ability
+  }
+
+  async validate(name: string): Promise<{ valid: boolean; errors: string[] }> {
+    await this.initialize()
+
+    const loaded = this.abilities.get(name)
+    if (!loaded) {
+      return { valid: false, errors: [`Ability '${name}' not found`] }
+    }
+
+    const result = validateAbility(loaded.ability)
+    return {
+      valid: result.valid,
+      errors: result.errors.map(e => `${e.path}: ${e.message}`),
+    }
+  }
+
+  async execute(
+    name: string,
+    inputs: InputValues = {},
+    context?: Partial<ExecutorContext>
+  ): Promise<ExecutionResult> {
+    await this.initialize()
+
+    const loaded = this.abilities.get(name)
+    if (!loaded) {
+      return {
+        id: '',
+        status: 'failed',
+        ability: name,
+        duration: 0,
+        steps: [],
+        error: `Ability '${name}' not found`,
+        formatted: `Error: Ability '${name}' not found`,
+      }
+    }
+
+    const ability = loaded.ability
+
+    const inputErrors = validateInputs(ability, inputs)
+    if (inputErrors.length > 0) {
+      return {
+        id: '',
+        status: 'failed',
+        ability: name,
+        duration: 0,
+        steps: [],
+        error: `Input validation failed: ${inputErrors.map(e => e.message).join(', ')}`,
+        formatted: `Input validation failed:\n${inputErrors.map(e => `- ${e.message}`).join('\n')}`,
+      }
+    }
+
+    const self = this
+    const executorContext: ExecutorContext = {
+      cwd: context?.cwd || process.cwd(),
+      env: context?.env || {},
+      agents: context?.agents,
+      skills: context?.skills,
+      approval: context?.approval,
+      abilities: {
+        get: (n: string) => self.abilities.get(n)?.ability,
+        execute: async (a: Ability, i: InputValues) => {
+          return executeAbility(a, i, executorContext)
+        },
+      },
+      onStepStart: context?.onStepStart,
+      onStepComplete: context?.onStepComplete,
+      onStepFail: context?.onStepFail,
+    }
+
+    try {
+      const execution = await this.executionManager.execute(ability, inputs, executorContext)
+
+      return {
+        id: execution.id,
+        status: execution.status === 'completed' ? 'completed' : execution.status === 'cancelled' ? 'cancelled' : 'failed',
+        ability: ability.name,
+        duration: execution.completedAt ? execution.completedAt - execution.startedAt : 0,
+        steps: execution.completedSteps.map(s => ({
+          id: s.stepId,
+          status: s.status,
+          duration: s.duration,
+          output: s.output,
+          error: s.error,
+        })),
+        error: execution.error,
+        formatted: formatExecutionResult(execution),
+      }
+    } catch (error) {
+      return {
+        id: '',
+        status: 'failed',
+        ability: name,
+        duration: 0,
+        steps: [],
+        error: error instanceof Error ? error.message : String(error),
+        formatted: `Execution error: ${error instanceof Error ? error.message : String(error)}`,
+      }
+    }
+  }
+
+  async status(executionId?: string): Promise<{
+    active: boolean
+    ability?: string
+    currentStep?: string
+    progress?: string
+    status?: string
+  }> {
+    const execution = executionId
+      ? this.executionManager.get(executionId)
+      : this.executionManager.getActive()
+
+    if (!execution) {
+      return { active: false }
+    }
+
+    return {
+      active: execution.status === 'running',
+      ability: execution.ability.name,
+      currentStep: execution.currentStep?.id,
+      progress: `${execution.completedSteps.length}/${execution.ability.steps.length}`,
+      status: execution.status,
+    }
+  }
+
+  async cancel(executionId?: string): Promise<boolean> {
+    if (executionId) {
+      return this.executionManager.cancel(executionId)
+    }
+    return this.executionManager.cancelActive()
+  }
+
+  async waitFor(executionId: string, timeoutMs: number = 300000): Promise<ExecutionResult | null> {
+    const startTime = Date.now()
+
+    while (Date.now() - startTime < timeoutMs) {
+      const execution = this.executionManager.get(executionId)
+
+      if (!execution) {
+        return null
+      }
+
+      if (execution.status !== 'running') {
+        return {
+          id: execution.id,
+          status: execution.status === 'completed' ? 'completed' : execution.status === 'cancelled' ? 'cancelled' : 'failed',
+          ability: execution.ability.name,
+          duration: execution.completedAt ? execution.completedAt - execution.startedAt : 0,
+          steps: execution.completedSteps.map(s => ({
+            id: s.stepId,
+            status: s.status,
+            duration: s.duration,
+            output: s.output,
+            error: s.error,
+          })),
+          error: execution.error,
+          formatted: formatExecutionResult(execution),
+        }
+      }
+
+      await new Promise(resolve => setTimeout(resolve, 100))
+    }
+
+    return null
+  }
+
+  cleanup(): void {
+    this.executionManager.cleanup()
+    this.abilities.clear()
+    this.initialized = false
+  }
+}
+
+export function createAbilitiesSDK(options?: AbilitiesSDKOptions): AbilitiesSDK {
+  return new AbilitiesSDK(options)
+}
+
+export { loadAbilities, listAbilities, validateAbility, validateInputs, executeAbility, formatExecutionResult }
+export type { Ability, AbilityExecution, ExecutorContext, LoadedAbility, InputValues }

+ 124 - 0
packages/plugin-abilities/src/types/index.ts

@@ -0,0 +1,124 @@
+/**
+ * Abilities System - Minimal Type Definitions
+ * 
+ * Stripped down to essentials for testing core concept:
+ * - Script steps only
+ * - Single execution tracking
+ * - No session management
+ */
+
+// ─────────────────────────────────────────────────────────────
+// INPUT TYPES
+// ─────────────────────────────────────────────────────────────
+
+export type InputType = 'string' | 'number' | 'boolean'
+
+export interface InputDefinition {
+  type: InputType
+  required?: boolean
+  default?: unknown
+  description?: string
+}
+
+export type InputValues = Record<string, unknown>
+
+// ─────────────────────────────────────────────────────────────
+// STEP TYPES (Script only for minimal version)
+// ─────────────────────────────────────────────────────────────
+
+export interface ScriptStep {
+  id: string
+  type: 'script'
+  description?: string
+  run: string
+  needs?: string[]
+  validation?: {
+    exit_code?: number
+  }
+}
+
+export type Step = ScriptStep
+
+// ─────────────────────────────────────────────────────────────
+// ABILITY DEFINITION
+// ─────────────────────────────────────────────────────────────
+
+export interface Ability {
+  name: string
+  description: string
+  inputs?: Record<string, InputDefinition>
+  steps: Step[]
+  _meta?: {
+    filePath: string
+    directory: string
+  }
+}
+
+// ─────────────────────────────────────────────────────────────
+// EXECUTION TYPES
+// ─────────────────────────────────────────────────────────────
+
+export type ExecutionStatus = 'running' | 'completed' | 'failed'
+export type StepStatus = 'completed' | 'failed' | 'skipped'
+
+export interface StepResult {
+  stepId: string
+  status: StepStatus
+  output?: string
+  error?: string
+  startedAt: number
+  completedAt: number
+  duration: number
+}
+
+export interface AbilityExecution {
+  id: string
+  ability: Ability
+  inputs: InputValues
+  status: ExecutionStatus
+  currentStep: Step | null
+  currentStepIndex: number
+  completedSteps: StepResult[]
+  pendingSteps: Step[]
+  startedAt: number
+  completedAt?: number
+  error?: string
+}
+
+// ─────────────────────────────────────────────────────────────
+// VALIDATION TYPES
+// ─────────────────────────────────────────────────────────────
+
+export interface ValidationError {
+  path: string
+  message: string
+}
+
+export interface ValidationResult {
+  valid: boolean
+  errors: ValidationError[]
+}
+
+// ─────────────────────────────────────────────────────────────
+// LOADER TYPES
+// ─────────────────────────────────────────────────────────────
+
+export interface LoaderOptions {
+  projectDir?: string
+  includeGlobal?: boolean
+}
+
+export interface LoadedAbility {
+  ability: Ability
+  filePath: string
+  source: 'project' | 'global'
+}
+
+// ─────────────────────────────────────────────────────────────
+// EXECUTOR TYPES
+// ─────────────────────────────────────────────────────────────
+
+export interface ExecutorContext {
+  cwd: string
+  env: Record<string, string>
+}

+ 356 - 0
packages/plugin-abilities/src/validator/index.ts

@@ -0,0 +1,356 @@
+import { z } from 'zod'
+import type {
+  Ability,
+  Step,
+  ValidationResult,
+  ValidationError,
+} from '../types/index.js'
+
+const InputDefinitionSchema = z.object({
+  type: z.enum(['string', 'number', 'boolean', 'array', 'object']),
+  required: z.boolean().optional(),
+  default: z.unknown().optional(),
+  description: z.string().optional(),
+  pattern: z.string().optional(),
+  enum: z.array(z.string()).optional(),
+  minLength: z.number().optional(),
+  maxLength: z.number().optional(),
+  min: z.number().optional(),
+  max: z.number().optional(),
+})
+
+const ValidationConfigSchema = z.object({
+  exit_code: z.number().optional(),
+  stdout_contains: z.string().optional(),
+  stderr_contains: z.string().optional(),
+  file_exists: z.string().optional(),
+})
+
+const BaseStepSchema = z.object({
+  id: z.string().regex(/^[a-z0-9-]+$/, 'Step ID must be lowercase alphanumeric with hyphens'),
+  description: z.string().optional(),
+  needs: z.array(z.string()).optional(),
+  when: z.string().optional(),
+  timeout: z.string().optional(),
+  on_failure: z.enum(['stop', 'continue', 'retry', 'ask']).optional(),
+  max_retries: z.number().optional(),
+})
+
+const ScriptStepSchema = BaseStepSchema.extend({
+  type: z.literal('script'),
+  run: z.string().min(1, 'Script command is required'),
+  cwd: z.string().optional(),
+  env: z.record(z.string()).optional(),
+  validation: ValidationConfigSchema.optional(),
+})
+
+const AgentStepSchema = BaseStepSchema.extend({
+  type: z.literal('agent'),
+  agent: z.string().min(1, 'Agent name is required'),
+  prompt: z.string().min(1, 'Prompt is required'),
+  context: z.array(z.string()).optional(),
+  summarize: z.union([z.boolean(), z.string()]).optional(),
+})
+
+const SkillStepSchema = BaseStepSchema.extend({
+  type: z.literal('skill'),
+  skill: z.string().min(1, 'Skill name is required'),
+  inputs: z.record(z.unknown()).optional(),
+})
+
+const ApprovalOptionSchema = z.object({
+  label: z.string(),
+  value: z.string(),
+})
+
+const ApprovalStepSchema = BaseStepSchema.extend({
+  type: z.literal('approval'),
+  prompt: z.string().min(1, 'Approval prompt is required'),
+  options: z.array(ApprovalOptionSchema).optional(),
+})
+
+const WorkflowStepSchema = BaseStepSchema.extend({
+  type: z.literal('workflow'),
+  workflow: z.string().min(1, 'Workflow name is required'),
+  inputs: z.record(z.unknown()).optional(),
+})
+
+const StepSchema = z.discriminatedUnion('type', [
+  ScriptStepSchema,
+  AgentStepSchema,
+  SkillStepSchema,
+  ApprovalStepSchema,
+  WorkflowStepSchema,
+])
+
+const TriggersSchema = z.object({
+  keywords: z.array(z.string()).optional(),
+  patterns: z.array(z.string()).optional(),
+})
+
+const SettingsSchema = z.object({
+  timeout: z.string().optional(),
+  parallel: z.boolean().optional(),
+  enforcement: z.enum(['strict', 'normal', 'loose']).optional(),
+  approval: z.enum(['plan', 'checkpoint', 'none']).optional(),
+  on_failure: z.enum(['stop', 'continue', 'retry', 'ask']).optional(),
+})
+
+const HooksSchema = z.object({
+  before: z.array(z.string()).optional(),
+  after: z.array(z.string()).optional(),
+})
+
+const AbilitySchema = z.object({
+  name: z.string().regex(
+    /^[a-z0-9-/]+$/,
+    'Name must be lowercase alphanumeric with hyphens and slashes'
+  ),
+  description: z.string().min(1, 'Description is required'),
+  version: z.string().optional(),
+  triggers: TriggersSchema.optional(),
+  inputs: z.record(InputDefinitionSchema).optional(),
+  steps: z.array(StepSchema).min(1, 'At least one step is required'),
+  settings: SettingsSchema.optional(),
+  hooks: HooksSchema.optional(),
+  compatible_agents: z.array(z.string()).optional(),
+  exclusive_agent: z.string().optional(),
+})
+
+function detectCircularDependencies(steps: Step[]): string[] | null {
+  const stepIds = new Set(steps.map((s) => s.id))
+  const visited = new Set<string>()
+  const recursionStack = new Set<string>()
+  const path: string[] = []
+
+  function dfs(stepId: string): string[] | null {
+    if (recursionStack.has(stepId)) {
+      const cycleStart = path.indexOf(stepId)
+      return [...path.slice(cycleStart), stepId]
+    }
+
+    if (visited.has(stepId)) {
+      return null
+    }
+
+    visited.add(stepId)
+    recursionStack.add(stepId)
+    path.push(stepId)
+
+    const step = steps.find((s) => s.id === stepId)
+    if (step?.needs) {
+      for (const dep of step.needs) {
+        const cycle = dfs(dep)
+        if (cycle) return cycle
+      }
+    }
+
+    path.pop()
+    recursionStack.delete(stepId)
+    return null
+  }
+
+  for (const step of steps) {
+    const cycle = dfs(step.id)
+    if (cycle) return cycle
+  }
+
+  return null
+}
+
+function validateDependencies(steps: Step[]): ValidationError[] {
+  const errors: ValidationError[] = []
+  const stepIds = new Set(steps.map((s) => s.id))
+
+  for (const step of steps) {
+    if (!step.needs) continue
+
+    for (const dep of step.needs) {
+      if (!stepIds.has(dep)) {
+        errors.push({
+          path: `steps.${step.id}.needs`,
+          message: `Dependency '${dep}' does not exist`,
+          code: 'MISSING_DEPENDENCY',
+        })
+      }
+    }
+  }
+
+  const cycle = detectCircularDependencies(steps)
+  if (cycle) {
+    errors.push({
+      path: 'steps',
+      message: `Circular dependency detected: ${cycle.join(' → ')}`,
+      code: 'CIRCULAR_DEPENDENCY',
+    })
+  }
+
+  return errors
+}
+
+function validateUniqueIds(steps: Step[]): ValidationError[] {
+  const errors: ValidationError[] = []
+  const seen = new Set<string>()
+
+  for (const step of steps) {
+    if (seen.has(step.id)) {
+      errors.push({
+        path: `steps.${step.id}`,
+        message: `Duplicate step ID '${step.id}'`,
+        code: 'DUPLICATE_ID',
+      })
+    }
+    seen.add(step.id)
+  }
+
+  return errors
+}
+
+export function validateAbility(data: unknown): ValidationResult {
+  const schemaResult = AbilitySchema.safeParse(data)
+
+  if (!schemaResult.success) {
+    return {
+      valid: false,
+      errors: schemaResult.error.errors.map((e) => ({
+        path: e.path.join('.'),
+        message: e.message,
+        code: 'SCHEMA_ERROR',
+      })),
+    }
+  }
+
+  const ability = schemaResult.data as Ability
+  const errors: ValidationError[] = []
+
+  errors.push(...validateUniqueIds(ability.steps))
+  errors.push(...validateDependencies(ability.steps))
+
+  if (ability.triggers?.patterns) {
+    for (const pattern of ability.triggers.patterns) {
+      try {
+        new RegExp(pattern)
+      } catch {
+        errors.push({
+          path: 'triggers.patterns',
+          message: `Invalid regex pattern: ${pattern}`,
+          code: 'INVALID_REGEX',
+        })
+      }
+    }
+  }
+
+  if (ability.inputs) {
+    for (const [name, input] of Object.entries(ability.inputs)) {
+      if (input.pattern) {
+        try {
+          new RegExp(input.pattern)
+        } catch {
+          errors.push({
+            path: `inputs.${name}.pattern`,
+            message: `Invalid regex pattern: ${input.pattern}`,
+            code: 'INVALID_REGEX',
+          })
+        }
+      }
+    }
+  }
+
+  return {
+    valid: errors.length === 0,
+    errors,
+    ability: errors.length === 0 ? ability : undefined,
+  }
+}
+
+export function validateInputs(
+  ability: Ability,
+  inputs: Record<string, unknown>
+): ValidationError[] {
+  const errors: ValidationError[] = []
+
+  if (!ability.inputs) return errors
+
+  for (const [name, definition] of Object.entries(ability.inputs)) {
+    const value = inputs[name]
+
+    if (definition.required && value === undefined && definition.default === undefined) {
+      errors.push({
+        path: `inputs.${name}`,
+        message: `Required input '${name}' is missing`,
+        code: 'MISSING_INPUT',
+      })
+      continue
+    }
+
+    if (value === undefined) continue
+
+    const expectedType = definition.type
+    const actualType = Array.isArray(value) ? 'array' : typeof value
+
+    if (actualType !== expectedType) {
+      errors.push({
+        path: `inputs.${name}`,
+        message: `Expected ${expectedType}, got ${actualType}`,
+        code: 'TYPE_MISMATCH',
+      })
+      continue
+    }
+
+    if (expectedType === 'string' && typeof value === 'string') {
+      if (definition.pattern && !new RegExp(definition.pattern).test(value)) {
+        errors.push({
+          path: `inputs.${name}`,
+          message: `Value '${value}' does not match pattern '${definition.pattern}'`,
+          code: 'PATTERN_MISMATCH',
+        })
+      }
+
+      if (definition.enum && !definition.enum.includes(value)) {
+        errors.push({
+          path: `inputs.${name}`,
+          message: `Value '${value}' must be one of: ${definition.enum.join(', ')}`,
+          code: 'ENUM_MISMATCH',
+        })
+      }
+
+      if (definition.minLength && value.length < definition.minLength) {
+        errors.push({
+          path: `inputs.${name}`,
+          message: `Value must be at least ${definition.minLength} characters`,
+          code: 'MIN_LENGTH',
+        })
+      }
+
+      if (definition.maxLength && value.length > definition.maxLength) {
+        errors.push({
+          path: `inputs.${name}`,
+          message: `Value must be at most ${definition.maxLength} characters`,
+          code: 'MAX_LENGTH',
+        })
+      }
+    }
+
+    if (expectedType === 'number' && typeof value === 'number') {
+      if (definition.min !== undefined && value < definition.min) {
+        errors.push({
+          path: `inputs.${name}`,
+          message: `Value must be at least ${definition.min}`,
+          code: 'MIN_VALUE',
+        })
+      }
+
+      if (definition.max !== undefined && value > definition.max) {
+        errors.push({
+          path: `inputs.${name}`,
+          message: `Value must be at most ${definition.max}`,
+          code: 'MAX_VALUE',
+        })
+      }
+    }
+  }
+
+  return errors
+}
+
+export { AbilitySchema, StepSchema }

+ 82 - 0
packages/plugin-abilities/test-minimal.ts

@@ -0,0 +1,82 @@
+#!/usr/bin/env bun
+/**
+ * Minimal Test Script
+ * 
+ * Quick validation that the minimal plugin works
+ */
+
+import { loadAbilities } from './src/loader/index.js'
+import { validateAbility } from './src/validator/index.js'
+import { executeAbility } from './src/executor/index.js'
+import type { ExecutorContext } from './src/types/index.js'
+
+async function test() {
+  console.log('🧪 Testing minimal abilities plugin...\n')
+
+  // Test 1: Load ability
+  console.log('Test 1: Loading test ability...')
+  const abilities = await loadAbilities({
+    projectDir: './examples/test',
+    includeGlobal: false,
+  })
+
+  if (abilities.size === 0) {
+    console.error('❌ No abilities loaded')
+    process.exit(1)
+  }
+
+  const testAbility = abilities.get('test')
+  if (!testAbility) {
+    console.error('❌ Test ability not found')
+    process.exit(1)
+  }
+  console.log('✅ Loaded ability:', testAbility.ability.name)
+  console.log()
+
+  // Test 2: Validate ability
+  console.log('Test 2: Validating ability...')
+  const validation = validateAbility(testAbility.ability)
+  if (!validation.valid) {
+    console.error('❌ Validation failed:', validation.errors)
+    process.exit(1)
+  }
+  console.log('✅ Ability is valid')
+  console.log()
+
+  // Test 3: Execute ability
+  console.log('Test 3: Executing ability...')
+  const ctx: ExecutorContext = {
+    cwd: process.cwd(),
+    env: {},
+  }
+
+  const execution = await executeAbility(
+    testAbility.ability,
+    { message: 'Test message from script' },
+    ctx
+  )
+
+  console.log('Status:', execution.status)
+  console.log('Steps completed:', execution.completedSteps.length)
+  console.log()
+
+  for (const step of execution.completedSteps) {
+    const icon = step.status === 'completed' ? '✅' : '❌'
+    console.log(`${icon} ${step.stepId}: ${step.status}`)
+    if (step.output) {
+      console.log(`   Output: ${step.output.trim()}`)
+    }
+  }
+
+  if (execution.status === 'completed') {
+    console.log('\n✅ All tests passed!')
+  } else {
+    console.log('\n❌ Execution failed:', execution.error)
+    process.exit(1)
+  }
+}
+
+test().catch((err) => {
+  console.error('❌ Test failed:', err)
+  process.exit(1)
+})

+ 87 - 0
packages/plugin-abilities/test-plugin.ts

@@ -0,0 +1,87 @@
+#!/usr/bin/env bun
+// Usage: bun test-plugin.ts
+
+import { createAbilitiesPlugin } from './src/plugin.js'
+
+async function main() {
+  console.log('🧪 Testing Abilities Plugin\n')
+  console.log('='.repeat(50))
+
+  const mockClient = {
+    session: {
+      get: async () => ({}),
+      list: async () => [],
+      command: async () => ({}),
+      prompt: async () => ({}),
+      todo: async () => ({})
+    },
+    events: {
+      publish: async (options: unknown) => {
+        console.log('[Event Published]', JSON.stringify(options, null, 2))
+      }
+    }
+  }
+
+  const mockContext = {
+    directory: process.cwd(),
+    worktree: process.cwd(),
+    client: mockClient,
+    $: () => ({ text: async () => '' })
+  }
+
+  console.log('\n📦 Initializing plugin...\n')
+  
+  const projectRoot = process.cwd().replace('/packages/plugin-abilities', '')
+  
+  const plugin = await createAbilitiesPlugin(mockContext as any, {
+    abilities: {
+      directories: [`${projectRoot}/.opencode/abilities`]
+    }
+  })
+
+  console.log('\n📋 Testing ability.list tool...\n')
+  const tools = plugin.tool
+  const listResult = await tools['ability.list'].execute({})
+  console.log(listResult)
+
+  console.log('\n='.repeat(50))
+  console.log('\n✅ Testing ability.validate tool...\n')
+  const validateResult = await tools['ability.validate'].execute({ name: 'hello-world' })
+  console.log(validateResult)
+
+  console.log('\n='.repeat(50))
+  console.log('\n🚀 Testing ability.run tool...\n')
+  const runResult = await tools['ability.run'].execute({ 
+    name: 'hello-world',
+    inputs: { name: 'OpenAgents' }
+  })
+  console.log(JSON.stringify(runResult, null, 2))
+
+  console.log('\n='.repeat(50))
+  console.log('\n📊 Testing ability.status tool...\n')
+  const statusResult = await tools['ability.status'].execute({})
+  console.log(JSON.stringify(statusResult, null, 2))
+
+  console.log('\n='.repeat(50))
+  console.log('\n🔍 Testing trigger detection (chat.message hook)...\n')
+  
+  const mockOutput = {
+    parts: [
+      { type: 'text', text: 'hello can you greet me?', synthetic: false }
+    ]
+  }
+  
+  await plugin['chat.message']({}, mockOutput)
+  
+  if (mockOutput.parts.length > 1) {
+    console.log('✅ Ability detected! Injected message:')
+    console.log(mockOutput.parts[0].text)
+  } else {
+    console.log('❌ No ability detected')
+  }
+
+  console.log('\n='.repeat(50))
+  console.log('\n✅ All tests completed!')
+}
+
+main().catch(console.error)

+ 46 - 0
packages/plugin-abilities/test-run-ability.ts

@@ -0,0 +1,46 @@
+#!/usr/bin/env bun
+
+import { createAbilitiesPlugin } from './src/plugin.js'
+
+async function main() {
+  console.log('🧪 Running test-abilities ability\n')
+
+  const mockClient = {
+    session: {
+      get: async () => ({}),
+      list: async () => [],
+      command: async () => ({}),
+      prompt: async () => ({}),
+      todo: async () => ({})
+    },
+    events: {
+      publish: async () => {}
+    }
+  }
+
+  const projectRoot = process.cwd().replace('/packages/plugin-abilities', '')
+
+  const mockContext = {
+    directory: projectRoot,
+    worktree: projectRoot,
+    client: mockClient,
+    $: () => ({ text: async () => '' })
+  }
+
+  const plugin = await createAbilitiesPlugin(mockContext as any, {
+    abilities: {
+      directories: [`${projectRoot}/.opencode/abilities`]
+    }
+  })
+
+  console.log('Running test-abilities...\n')
+  
+  const result = await plugin.tool['ability.run'].execute({ 
+    name: 'test-abilities',
+    inputs: {}
+  })
+  
+  console.log(JSON.stringify(result, null, 2))
+}
+
+main().catch(console.error)

+ 311 - 0
packages/plugin-abilities/tests/context-passing.test.ts

@@ -0,0 +1,311 @@
+import { describe, test, expect } from 'bun:test'
+import { executeAbility } from '../src/executor/index.js'
+import type { Ability, ExecutorContext } from '../src/types/index.js'
+
+describe('Context Passing', () => {
+  describe('Auto-truncation', () => {
+    test('should truncate large outputs when passing to agent steps', async () => {
+      let receivedPrompt = ''
+
+      const ability: Ability = {
+        name: 'test-truncation',
+        description: 'Test output truncation',
+        steps: [
+          { id: 'generate', type: 'script', run: 'node -e "console.log(\'x\'.repeat(100000))"' },
+          { id: 'use', type: 'agent', agent: 'test', prompt: 'Process the output', needs: ['generate'] }
+        ]
+      }
+
+      const ctx: ExecutorContext = {
+        cwd: '/tmp',
+        env: {},
+        agents: {
+          async call(options) { 
+            receivedPrompt = options.prompt
+            return 'done' 
+          },
+          async background() { return 'done' }
+        }
+      }
+
+      const execution = await executeAbility(ability, {}, ctx)
+
+      expect(execution.status).toBe('completed')
+      expect(receivedPrompt).toContain('truncated')
+    })
+
+    test('should preserve small outputs fully', async () => {
+      const smallOutput = 'Hello World'
+
+      const ability: Ability = {
+        name: 'test-small',
+        description: 'Test small output',
+        steps: [
+          { id: 'step1', type: 'script', run: `echo "${smallOutput}"` }
+        ]
+      }
+
+      const ctx: ExecutorContext = {
+        cwd: '/tmp',
+        env: {}
+      }
+
+      const execution = await executeAbility(ability, {}, ctx)
+
+      const step1Result = execution.completedSteps.find(s => s.stepId === 'step1')
+      expect(step1Result?.output).toContain(smallOutput)
+    })
+  })
+
+  describe('Variable Interpolation', () => {
+    test('should interpolate input variables', async () => {
+      const ability: Ability = {
+        name: 'test-interpolation',
+        description: 'Test variable interpolation',
+        inputs: {
+          name: { type: 'string', required: true }
+        },
+        steps: [
+          { id: 'greet', type: 'script', run: 'echo "Hello {{inputs.name}}"' }
+        ]
+      }
+
+      const ctx: ExecutorContext = {
+        cwd: '/tmp',
+        env: {}
+      }
+
+      const execution = await executeAbility(ability, { name: 'World' }, ctx)
+
+      expect(execution.status).toBe('completed')
+    })
+
+    test('should interpolate step outputs', async () => {
+      const ability: Ability = {
+        name: 'test-step-output',
+        description: 'Test step output interpolation',
+        steps: [
+          { id: 'first', type: 'script', run: 'echo "FirstOutput"' },
+          { id: 'second', type: 'script', run: 'echo "Got: {{steps.first.output}}"', needs: ['first'] }
+        ]
+      }
+
+      const ctx: ExecutorContext = {
+        cwd: '/tmp',
+        env: {}
+      }
+
+      const execution = await executeAbility(ability, {}, ctx)
+
+      expect(execution.status).toBe('completed')
+      expect(execution.completedSteps.length).toBe(2)
+    })
+  })
+
+  describe('Summarization', () => {
+    test('should summarize output when summarize flag is set', async () => {
+      let secondStepPrompt = ''
+      const longOutput = 'Line\n'.repeat(100)
+
+      const ability: Ability = {
+        name: 'test-summarize',
+        description: 'Test summarization',
+        steps: [
+          {
+            id: 'research',
+            type: 'agent',
+            agent: 'librarian',
+            prompt: 'Research topic',
+            summarize: true
+          },
+          {
+            id: 'use',
+            type: 'agent',
+            agent: 'oracle',
+            prompt: 'Use the research',
+            needs: ['research']
+          }
+        ]
+      }
+
+      let callCount = 0
+      const ctx: ExecutorContext = {
+        cwd: '/tmp',
+        env: {},
+        agents: {
+          async call(options) { 
+            callCount++
+            if (callCount === 2) {
+              secondStepPrompt = options.prompt
+            }
+            return longOutput 
+          },
+          async background() { return longOutput }
+        }
+      }
+
+      const execution = await executeAbility(ability, {}, ctx)
+
+      expect(execution.status).toBe('completed')
+      expect(secondStepPrompt).toContain('Output Summary')
+      expect(secondStepPrompt).toContain('lines omitted')
+    })
+  })
+})
+
+describe('Nested Workflows', () => {
+  test('should fail without abilities context', async () => {
+    const ability: Ability = {
+      name: 'test-nested',
+      description: 'Test nested workflow',
+      steps: [
+        { id: 'nested', type: 'workflow', workflow: 'child-ability' }
+      ]
+    }
+
+    const ctx: ExecutorContext = {
+      cwd: '/tmp',
+      env: {}
+    }
+
+    const execution = await executeAbility(ability, {}, ctx)
+
+    expect(execution.status).toBe('failed')
+    expect(execution.completedSteps[0].error).toContain('not available')
+  })
+
+  test('should fail when nested ability not found', async () => {
+    const ability: Ability = {
+      name: 'test-nested-missing',
+      description: 'Test missing nested ability',
+      steps: [
+        { id: 'nested', type: 'workflow', workflow: 'nonexistent' }
+      ]
+    }
+
+    const ctx: ExecutorContext = {
+      cwd: '/tmp',
+      env: {},
+      abilities: {
+        get: () => undefined,
+        execute: async () => ({ 
+          id: 'x', 
+          ability: ability, 
+          inputs: {}, 
+          status: 'completed', 
+          currentStep: null, 
+          currentStepIndex: -1, 
+          completedSteps: [], 
+          pendingSteps: [], 
+          startedAt: Date.now() 
+        })
+      }
+    }
+
+    const execution = await executeAbility(ability, {}, ctx)
+
+    expect(execution.status).toBe('failed')
+    expect(execution.completedSteps[0].error).toContain('not found')
+  })
+
+  test('should execute nested ability successfully', async () => {
+    const childAbility: Ability = {
+      name: 'child',
+      description: 'Child ability',
+      steps: [
+        { id: 'child-step', type: 'script', run: 'echo "child done"' }
+      ]
+    }
+
+    const parentAbility: Ability = {
+      name: 'parent',
+      description: 'Parent ability',
+      steps: [
+        { id: 'call-child', type: 'workflow', workflow: 'child' }
+      ]
+    }
+
+    const ctx: ExecutorContext = {
+      cwd: '/tmp',
+      env: {},
+      abilities: {
+        get: (name) => name === 'child' ? childAbility : undefined,
+        execute: async (ability, inputs) => {
+          return {
+            id: 'nested-exec',
+            ability,
+            inputs,
+            status: 'completed',
+            currentStep: null,
+            currentStepIndex: -1,
+            completedSteps: [
+              { stepId: 'child-step', status: 'completed', output: 'child done', startedAt: Date.now(), completedAt: Date.now(), duration: 10 }
+            ],
+            pendingSteps: [],
+            startedAt: Date.now(),
+            completedAt: Date.now()
+          }
+        }
+      }
+    }
+
+    const execution = await executeAbility(parentAbility, {}, ctx)
+
+    expect(execution.status).toBe('completed')
+    expect(execution.completedSteps[0].output).toContain('completed successfully')
+  })
+
+  test('should pass inputs to nested workflow', async () => {
+    let receivedInputs: Record<string, unknown> = {}
+
+    const childAbility: Ability = {
+      name: 'child',
+      description: 'Child ability',
+      inputs: { env: { type: 'string', required: true } },
+      steps: [
+        { id: 'child-step', type: 'script', run: 'echo "done"' }
+      ]
+    }
+
+    const parentAbility: Ability = {
+      name: 'parent',
+      description: 'Parent ability',
+      inputs: { environment: { type: 'string', required: true } },
+      steps: [
+        { 
+          id: 'call-child', 
+          type: 'workflow', 
+          workflow: 'child',
+          inputs: { env: '{{inputs.environment}}' }
+        }
+      ]
+    }
+
+    const ctx: ExecutorContext = {
+      cwd: '/tmp',
+      env: {},
+      abilities: {
+        get: (name) => name === 'child' ? childAbility : undefined,
+        execute: async (ability, inputs) => {
+          receivedInputs = inputs
+          return {
+            id: 'nested-exec',
+            ability,
+            inputs,
+            status: 'completed',
+            currentStep: null,
+            currentStepIndex: -1,
+            completedSteps: [],
+            pendingSteps: [],
+            startedAt: Date.now(),
+            completedAt: Date.now()
+          }
+        }
+      }
+    }
+
+    await executeAbility(parentAbility, { environment: 'production' }, ctx)
+
+    expect(receivedInputs.env).toBe('production')
+  })
+})

+ 272 - 0
packages/plugin-abilities/tests/enforcement.test.ts

@@ -0,0 +1,272 @@
+import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
+import { AbilitiesPlugin } from '../src/plugin.js'
+
+describe('Agent Attachment', () => {
+  let plugin: AbilitiesPlugin
+
+  beforeEach(() => {
+    plugin = new AbilitiesPlugin()
+  })
+
+  afterEach(() => {
+    plugin.cleanup()
+  })
+
+  test('should register agent abilities but only return loaded ones', async () => {
+    await plugin.initialize(
+      { directory: '.', worktree: '.', client: null as any, $: null as any },
+      {}
+    )
+
+    // Register ability names - note: these abilities don't exist in the plugin
+    plugin.registerAgentAbilities('test-agent', ['deploy', 'test-suite'])
+    const abilities = plugin.getAgentAbilities('test-agent')
+
+    // Should return 0 because 'deploy' and 'test-suite' abilities aren't loaded
+    // This tests that getAgentAbilities only returns abilities that actually exist
+    expect(abilities).toHaveLength(0)
+  })
+
+  test('should set current agent', async () => {
+    await plugin.initialize(
+      { directory: '.', worktree: '.', client: null as any, $: null as any },
+      {}
+    )
+
+    expect(plugin.getCurrentAgent()).toBeUndefined()
+
+    plugin.setCurrentAgent('openagent')
+    expect(plugin.getCurrentAgent()).toBe('openagent')
+
+    plugin.setCurrentAgent(undefined)
+    expect(plugin.getCurrentAgent()).toBeUndefined()
+  })
+
+  test('should check ability-agent compatibility', async () => {
+    await plugin.initialize(
+      { directory: '.', worktree: '.', client: null as any, $: null as any },
+      {}
+    )
+
+    expect(plugin.isAbilityAllowedForAgent('nonexistent', 'any-agent')).toBe(false)
+  })
+
+  test('should handle agent.changed event', async () => {
+    await plugin.initialize(
+      { directory: '.', worktree: '.', client: null as any, $: null as any },
+      {}
+    )
+
+    await plugin.handleEvent({
+      event: {
+        type: 'agent.changed',
+        properties: {
+          agent: {
+            id: 'test-agent',
+            abilities: ['deploy', 'review']
+          }
+        }
+      }
+    })
+
+    expect(plugin.getCurrentAgent()).toBe('test-agent')
+  })
+})
+
+describe('Enforcement Hooks', () => {
+  let plugin: AbilitiesPlugin
+
+  beforeEach(() => {
+    plugin = new AbilitiesPlugin()
+  })
+
+  afterEach(() => {
+    plugin.cleanup()
+  })
+
+  describe('tool.execute.before', () => {
+    test('should allow all tools when no ability is active', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        {}
+      )
+
+      await expect(
+        plugin.handleToolExecuteBefore({ tool: 'bash' }, { args: {} })
+      ).resolves.toBeUndefined()
+
+      await expect(
+        plugin.handleToolExecuteBefore({ tool: 'write' }, { args: {} })
+      ).resolves.toBeUndefined()
+    })
+
+    test('should always allow status tools', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        {}
+      )
+
+      const alwaysAllowed = ['ability.list', 'ability.status', 'todoread', 'read', 'glob', 'grep']
+
+      for (const tool of alwaysAllowed) {
+        await expect(
+          plugin.handleToolExecuteBefore({ tool }, { args: {} })
+        ).resolves.toBeUndefined()
+      }
+    })
+
+    test('should respect loose enforcement mode', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        { abilities: { enforcement: 'loose' } }
+      )
+
+      await expect(
+        plugin.handleToolExecuteBefore({ tool: 'bash' }, { args: {} })
+      ).resolves.toBeUndefined()
+    })
+  })
+
+  describe('chat.message injection', () => {
+    test('should not inject when no ability is active', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        {}
+      )
+
+      const output = {
+        parts: [{ type: 'text', text: 'Hello world' }]
+      }
+
+      await plugin.handleChatMessage({}, output as any)
+
+      expect(output.parts.length).toBe(1)
+      expect(output.parts[0].text).toBe('Hello world')
+    })
+
+    test('should not inject when no abilities match keywords', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        { abilities: { auto_trigger: true } }
+      )
+
+      const output = {
+        parts: [{ type: 'text', text: 'Please deploy the application' }]
+      }
+
+      await plugin.handleChatMessage({}, output as any)
+
+      const syntheticParts = output.parts.filter((p: any) => p.synthetic)
+      expect(syntheticParts).toHaveLength(0)
+    })
+  })
+
+  describe('session.idle', () => {
+    test('should return empty when no ability is active', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        {}
+      )
+
+      const result = await plugin.handleSessionIdle()
+      expect(result).toEqual({})
+    })
+  })
+
+  describe('getStepTypeBlockMessage', () => {
+    test('should return correct message for each step type', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        {}
+      )
+
+      const getMessage = (plugin as any).getStepTypeBlockMessage.bind(plugin)
+
+      expect(getMessage({ type: 'script', id: 'test' })).toContain('deterministically')
+      expect(getMessage({ type: 'agent', id: 'test' })).toContain('agent invocation')
+      expect(getMessage({ type: 'approval', id: 'test' })).toContain('approval')
+      expect(getMessage({ type: 'skill', id: 'test' })).toContain('skill')
+      expect(getMessage({ type: 'workflow', id: 'test' })).toContain('ability')
+    })
+  })
+
+  describe('getStepInstructions', () => {
+    test('should return correct instructions for each step type', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        {}
+      )
+
+      const getInstructions = (plugin as any).getStepInstructions.bind(plugin)
+
+      expect(getInstructions({ type: 'script', id: 'test', run: 'echo hi' }))
+        .toContain('Script is executing')
+
+      expect(getInstructions({ type: 'agent', id: 'test', agent: 'reviewer', prompt: 'Review code' }))
+        .toContain('Invoke agent "reviewer"')
+
+      expect(getInstructions({ type: 'skill', id: 'test', skill: 'commit' }))
+        .toContain('skill "commit"')
+
+      expect(getInstructions({ type: 'approval', id: 'test', prompt: 'Deploy?' }))
+        .toContain('Request user approval')
+
+      expect(getInstructions({ type: 'workflow', id: 'test', workflow: 'deploy' }))
+        .toContain('nested ability "deploy"')
+    })
+  })
+
+  describe('buildAbilityContextInjection', () => {
+    test('should build correct context for active ability', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        { abilities: { enforcement: 'strict' } }
+      )
+
+      const mockExecution = {
+        ability: {
+          name: 'test-ability',
+          description: 'Test ability',
+          steps: [
+            { id: 'step1', type: 'script', run: 'echo hi' },
+            { id: 'step2', type: 'agent', agent: 'reviewer', prompt: 'Review' }
+          ]
+        },
+        currentStep: { id: 'step1', type: 'script', run: 'echo hi', description: 'Run tests' },
+        completedSteps: [],
+        status: 'running'
+      }
+
+      const buildContext = (plugin as any).buildAbilityContextInjection.bind(plugin)
+      const result = buildContext(mockExecution)
+
+      expect(result).toContain('Active Ability: test-ability')
+      expect(result).toContain('0/2 steps completed')
+      expect(result).toContain('Current Step: step1')
+      expect(result).toContain('[STRICT MODE]')
+    })
+
+    test('should not show strict mode in normal enforcement', async () => {
+      await plugin.initialize(
+        { directory: '.', worktree: '.', client: null as any, $: null as any },
+        { abilities: { enforcement: 'normal' } }
+      )
+
+      const mockExecution = {
+        ability: {
+          name: 'test-ability',
+          description: 'Test ability',
+          steps: [{ id: 'step1', type: 'script', run: 'echo hi' }]
+        },
+        currentStep: { id: 'step1', type: 'script', run: 'echo hi' },
+        completedSteps: [],
+        status: 'running'
+      }
+
+      const buildContext = (plugin as any).buildAbilityContextInjection.bind(plugin)
+      const result = buildContext(mockExecution)
+
+      expect(result).not.toContain('[STRICT MODE]')
+    })
+  })
+})

+ 471 - 0
packages/plugin-abilities/tests/executor.test.ts

@@ -0,0 +1,471 @@
+import { describe, expect, it } from 'bun:test'
+import { executeAbility, formatExecutionResult } from '../src/executor/index.js'
+import type { Ability, ExecutorContext } from '../src/types/index.js'
+
+const createMockContext = (): ExecutorContext => ({
+  cwd: process.cwd(),
+  env: {},
+})
+
+describe('executeAbility', () => {
+  it('should execute a simple script ability', async () => {
+    const ability: Ability = {
+      name: 'simple',
+      description: 'Simple test',
+      steps: [
+        {
+          id: 'echo',
+          type: 'script',
+          run: 'echo "hello world"',
+          validation: {
+            exit_code: 0,
+          },
+        },
+      ],
+    }
+
+    const result = await executeAbility(ability, {}, createMockContext())
+
+    expect(result.status).toBe('completed')
+    expect(result.completedSteps).toHaveLength(1)
+    expect(result.completedSteps[0].status).toBe('completed')
+    expect(result.completedSteps[0].output).toContain('hello world')
+  })
+
+  it('should execute steps in dependency order', async () => {
+    const executionOrder: string[] = []
+
+    const ability: Ability = {
+      name: 'ordered',
+      description: 'Ordered test',
+      steps: [
+        {
+          id: 'third',
+          type: 'script',
+          run: 'echo third',
+          needs: ['second'],
+        },
+        {
+          id: 'first',
+          type: 'script',
+          run: 'echo first',
+        },
+        {
+          id: 'second',
+          type: 'script',
+          run: 'echo second',
+          needs: ['first'],
+        },
+      ],
+    }
+
+    const ctx = createMockContext()
+    ctx.onStepStart = (step) => executionOrder.push(step.id)
+
+    await executeAbility(ability, {}, ctx)
+
+    expect(executionOrder).toEqual(['first', 'second', 'third'])
+  })
+
+  it('should fail on script validation failure', async () => {
+    const ability: Ability = {
+      name: 'fail',
+      description: 'Fail test',
+      steps: [
+        {
+          id: 'fail',
+          type: 'script',
+          run: 'exit 1',
+          validation: {
+            exit_code: 0,
+          },
+        },
+      ],
+    }
+
+    const result = await executeAbility(ability, {}, createMockContext())
+
+    expect(result.status).toBe('failed')
+    expect(result.completedSteps[0].status).toBe('failed')
+  })
+
+  it('should validate inputs before execution', async () => {
+    const ability: Ability = {
+      name: 'with-inputs',
+      description: 'Input test',
+      inputs: {
+        name: {
+          type: 'string',
+          required: true,
+        },
+      },
+      steps: [
+        {
+          id: 'greet',
+          type: 'script',
+          run: 'echo "Hello {{inputs.name}}"',
+        },
+      ],
+    }
+
+    const result = await executeAbility(ability, {}, createMockContext())
+
+    expect(result.status).toBe('failed')
+    expect(result.error).toContain('Input validation failed')
+  })
+
+  it('should interpolate variables in script commands', async () => {
+    const ability: Ability = {
+      name: 'interpolate',
+      description: 'Interpolation test',
+      inputs: {
+        name: {
+          type: 'string',
+          required: true,
+        },
+      },
+      steps: [
+        {
+          id: 'greet',
+          type: 'script',
+          run: 'echo "Hello {{inputs.name}}"',
+        },
+      ],
+    }
+
+    const result = await executeAbility(
+      ability,
+      { name: 'World' },
+      createMockContext()
+    )
+
+    expect(result.status).toBe('completed')
+    expect(result.completedSteps[0].output).toContain('Hello World')
+  })
+
+  it('should skip steps when condition is not met', async () => {
+    const ability: Ability = {
+      name: 'conditional',
+      description: 'Conditional test',
+      inputs: {
+        env: {
+          type: 'string',
+          required: true,
+        },
+      },
+      steps: [
+        {
+          id: 'always',
+          type: 'script',
+          run: 'echo always',
+        },
+        {
+          id: 'prod-only',
+          type: 'script',
+          run: 'echo production',
+          when: 'inputs.env == "production"',
+        },
+      ],
+    }
+
+    const result = await executeAbility(
+      ability,
+      { env: 'staging' },
+      createMockContext()
+    )
+
+    expect(result.status).toBe('completed')
+    expect(result.completedSteps[0].status).toBe('completed')
+    expect(result.completedSteps[1].status).toBe('skipped')
+  })
+
+  it('should continue on failure when configured', async () => {
+    const ability: Ability = {
+      name: 'continue',
+      description: 'Continue test',
+      steps: [
+        {
+          id: 'fail',
+          type: 'script',
+          run: 'exit 1',
+          validation: {
+            exit_code: 0,
+          },
+          on_failure: 'continue',
+        },
+        {
+          id: 'after',
+          type: 'script',
+          run: 'echo after',
+        },
+      ],
+    }
+
+    const result = await executeAbility(ability, {}, createMockContext())
+
+    expect(result.status).toBe('completed')
+    expect(result.completedSteps[0].status).toBe('failed')
+    expect(result.completedSteps[1].status).toBe('completed')
+  })
+})
+
+describe('executeAgentStep', () => {
+  it('should execute agent step with context', async () => {
+    const ability: Ability = {
+      name: 'agent-test',
+      description: 'Agent test',
+      steps: [
+        {
+          id: 'review',
+          type: 'agent',
+          agent: 'reviewer',
+          prompt: 'Review this code',
+        },
+      ],
+    }
+
+    const ctx = createMockContext()
+    ctx.agents = {
+      async call(options) {
+        return `Reviewed by ${options.agent}: ${options.prompt}`
+      },
+      async background(options) {
+        return this.call(options)
+      }
+    }
+
+    const result = await executeAbility(ability, {}, ctx)
+
+    expect(result.status).toBe('completed')
+    expect(result.completedSteps[0].status).toBe('completed')
+    expect(result.completedSteps[0].output).toContain('Reviewed by reviewer')
+  })
+
+  it('should fail agent step without agent context', async () => {
+    const ability: Ability = {
+      name: 'agent-fail',
+      description: 'Agent fail test',
+      steps: [
+        {
+          id: 'review',
+          type: 'agent',
+          agent: 'reviewer',
+          prompt: 'Review this',
+        },
+      ],
+    }
+
+    const result = await executeAbility(ability, {}, createMockContext())
+
+    expect(result.status).toBe('failed')
+    expect(result.completedSteps[0].error).toContain('Agent execution not available')
+  })
+
+  it('should pass prior step outputs to agent step', async () => {
+    const ability: Ability = {
+      name: 'context-pass',
+      description: 'Context passing test',
+      steps: [
+        {
+          id: 'generate',
+          type: 'script',
+          run: 'echo "GENERATED_DATA_123"',
+        },
+        {
+          id: 'review',
+          type: 'agent',
+          agent: 'reviewer',
+          prompt: 'Review the generated data',
+          needs: ['generate'],
+        },
+      ],
+    }
+
+    let receivedPrompt = ''
+    const ctx = createMockContext()
+    ctx.agents = {
+      async call(options) {
+        receivedPrompt = options.prompt
+        return 'Reviewed'
+      },
+      async background(options) {
+        return this.call(options)
+      }
+    }
+
+    await executeAbility(ability, {}, ctx)
+
+    expect(receivedPrompt).toContain('Context from prior steps')
+    expect(receivedPrompt).toContain('generate')
+    expect(receivedPrompt).toContain('GENERATED_DATA_123')
+  })
+})
+
+describe('executeSkillStep', () => {
+  it('should execute skill step with context', async () => {
+    const ability: Ability = {
+      name: 'skill-test',
+      description: 'Skill test',
+      steps: [
+        {
+          id: 'docs',
+          type: 'skill',
+          skill: 'generate-docs',
+        },
+      ],
+    }
+
+    const ctx = createMockContext()
+    ctx.skills = {
+      async load(name) {
+        return `Loaded skill: ${name}`
+      }
+    }
+
+    const result = await executeAbility(ability, {}, ctx)
+
+    expect(result.status).toBe('completed')
+    expect(result.completedSteps[0].output).toContain('generate-docs')
+  })
+
+  it('should fail skill step without skill context', async () => {
+    const ability: Ability = {
+      name: 'skill-fail',
+      description: 'Skill fail test',
+      steps: [
+        {
+          id: 'docs',
+          type: 'skill',
+          skill: 'generate-docs',
+        },
+      ],
+    }
+
+    const result = await executeAbility(ability, {}, createMockContext())
+
+    expect(result.status).toBe('failed')
+    expect(result.completedSteps[0].error).toContain('Skill execution not available')
+  })
+})
+
+describe('executeApprovalStep', () => {
+  it('should execute approval step when approved', async () => {
+    const ability: Ability = {
+      name: 'approval-test',
+      description: 'Approval test',
+      steps: [
+        {
+          id: 'approve',
+          type: 'approval',
+          prompt: 'Deploy to production?',
+        },
+      ],
+    }
+
+    const ctx = createMockContext()
+    ctx.approval = {
+      async request() {
+        return true
+      }
+    }
+
+    const result = await executeAbility(ability, {}, ctx)
+
+    expect(result.status).toBe('completed')
+    expect(result.completedSteps[0].output).toBe('Approved')
+  })
+
+  it('should fail approval step when rejected', async () => {
+    const ability: Ability = {
+      name: 'approval-reject',
+      description: 'Approval reject test',
+      steps: [
+        {
+          id: 'approve',
+          type: 'approval',
+          prompt: 'Deploy to production?',
+        },
+      ],
+    }
+
+    const ctx = createMockContext()
+    ctx.approval = {
+      async request() {
+        return false
+      }
+    }
+
+    const result = await executeAbility(ability, {}, ctx)
+
+    expect(result.status).toBe('failed')
+    expect(result.completedSteps[0].output).toBe('Rejected')
+  })
+
+  it('should fail approval step without approval context', async () => {
+    const ability: Ability = {
+      name: 'approval-fail',
+      description: 'Approval fail test',
+      steps: [
+        {
+          id: 'approve',
+          type: 'approval',
+          prompt: 'Deploy?',
+        },
+      ],
+    }
+
+    const result = await executeAbility(ability, {}, createMockContext())
+
+    expect(result.status).toBe('failed')
+    expect(result.completedSteps[0].error).toContain('Approval not available')
+  })
+
+  it('should interpolate variables in approval prompt', async () => {
+    const ability: Ability = {
+      name: 'approval-vars',
+      description: 'Approval vars test',
+      inputs: {
+        version: { type: 'string', required: true }
+      },
+      steps: [
+        {
+          id: 'approve',
+          type: 'approval',
+          prompt: 'Deploy {{inputs.version}} to production?',
+        },
+      ],
+    }
+
+    let receivedPrompt = ''
+    const ctx = createMockContext()
+    ctx.approval = {
+      async request(options) {
+        receivedPrompt = options.prompt
+        return true
+      }
+    }
+
+    await executeAbility(ability, { version: 'v1.2.3' }, ctx)
+
+    expect(receivedPrompt).toBe('Deploy v1.2.3 to production?')
+  })
+})
+
+describe('formatExecutionResult', () => {
+  it('should format completed execution', async () => {
+    const ability: Ability = {
+      name: 'test',
+      description: 'Test',
+      steps: [
+        { id: 'step1', type: 'script', run: 'echo hello' },
+      ],
+    }
+
+    const execution = await executeAbility(ability, {}, createMockContext())
+    const formatted = formatExecutionResult(execution)
+
+    expect(formatted).toContain('Ability: test')
+    expect(formatted).toContain('✅ Complete')
+    expect(formatted).toContain('✅ step1')
+  })
+})

+ 300 - 0
packages/plugin-abilities/tests/integration.test.ts

@@ -0,0 +1,300 @@
+import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
+import * as fs from 'fs/promises'
+import * as path from 'path'
+import { loadAbilities, loadAbility } from '../src/loader/index.js'
+import { validateAbility } from '../src/validator/index.js'
+import { executeAbility } from '../src/executor/index.js'
+import { ExecutionManager } from '../src/executor/execution-manager.js'
+import type { Ability, ExecutorContext } from '../src/types/index.js'
+
+const TEST_ABILITIES_DIR = path.join(process.cwd(), 'test-abilities')
+
+const createMockContext = (): ExecutorContext => ({
+  cwd: process.cwd(),
+  env: {},
+})
+
+describe('Integration: Full Ability Lifecycle', () => {
+  beforeEach(async () => {
+    await fs.mkdir(TEST_ABILITIES_DIR, { recursive: true })
+  })
+
+  afterEach(async () => {
+    await fs.rm(TEST_ABILITIES_DIR, { recursive: true, force: true })
+  })
+
+  it('should load, validate, and execute an ability from disk', async () => {
+    const abilityYaml = `
+name: test-integration
+description: Integration test ability
+
+inputs:
+  message:
+    type: string
+    required: true
+
+steps:
+  - id: echo
+    type: script
+    run: echo "{{inputs.message}}"
+    validation:
+      exit_code: 0
+`
+    await fs.writeFile(
+      path.join(TEST_ABILITIES_DIR, 'test-integration.yaml'),
+      abilityYaml
+    )
+
+    const abilities = await loadAbilities({
+      projectDir: TEST_ABILITIES_DIR,
+      includeGlobal: false,
+    })
+
+    expect(abilities.size).toBe(1)
+
+    const loaded = abilities.get('test-integration')
+    expect(loaded).toBeDefined()
+    expect(loaded!.ability.name).toBe('test-integration')
+
+    const validationResult = validateAbility(loaded!.ability)
+    expect(validationResult.valid).toBe(true)
+
+    const execution = await executeAbility(
+      loaded!.ability,
+      { message: 'Hello Integration' },
+      createMockContext()
+    )
+
+    expect(execution.status).toBe('completed')
+    expect(execution.completedSteps).toHaveLength(1)
+    expect(execution.completedSteps[0].output).toContain('Hello Integration')
+  })
+
+  it('should load abilities from nested directories', async () => {
+    await fs.mkdir(path.join(TEST_ABILITIES_DIR, 'deploy', 'staging'), { recursive: true })
+
+    const abilityYaml = `
+name: deploy/staging
+description: Deploy to staging
+
+steps:
+  - id: deploy
+    type: script
+    run: echo "Deploying to staging"
+`
+    await fs.writeFile(
+      path.join(TEST_ABILITIES_DIR, 'deploy', 'staging', 'ability.yaml'),
+      abilityYaml
+    )
+
+    const abilities = await loadAbilities({
+      projectDir: TEST_ABILITIES_DIR,
+      includeGlobal: false,
+    })
+
+    expect(abilities.size).toBe(1)
+
+    const loaded = abilities.get('deploy/staging')
+    expect(loaded).toBeDefined()
+  })
+
+  it('should reject invalid abilities during validation', async () => {
+    const invalidYaml = `
+name: invalid-ability
+description: Missing name field actually has name, but empty steps
+steps: []
+`
+    await fs.writeFile(
+      path.join(TEST_ABILITIES_DIR, 'invalid.yaml'),
+      invalidYaml
+    )
+
+    const abilities = await loadAbilities({
+      projectDir: TEST_ABILITIES_DIR,
+      includeGlobal: false,
+    })
+
+    const loaded = abilities.get('invalid-ability')
+    expect(loaded).toBeDefined()
+
+    const result = validateAbility(loaded!.ability)
+    expect(result.valid).toBe(false)
+  })
+})
+
+describe('Integration: ExecutionManager', () => {
+  let manager: ExecutionManager
+
+  beforeEach(() => {
+    manager = new ExecutionManager()
+  })
+
+  afterEach(() => {
+    manager.cleanup()
+  })
+
+  it('should track and manage execution lifecycle', async () => {
+    const ability: Ability = {
+      name: 'managed-test',
+      description: 'Test managed execution',
+      steps: [
+        { id: 'step1', type: 'script', run: 'echo step1' },
+        { id: 'step2', type: 'script', run: 'echo step2', needs: ['step1'] },
+      ],
+    }
+
+    const execution = await manager.execute(ability, {}, createMockContext())
+
+    expect(execution.status).toBe('completed')
+    expect(manager.get(execution.id)).toBeDefined()
+    expect(manager.list()).toHaveLength(1)
+  })
+
+  it('should prevent concurrent executions', async () => {
+    const slowAbility: Ability = {
+      name: 'slow-test',
+      description: 'Slow test',
+      steps: [
+        { id: 'slow', type: 'script', run: 'sleep 0.1' },
+      ],
+    }
+
+    const fastAbility: Ability = {
+      name: 'fast-test',
+      description: 'Fast test',
+      steps: [
+        { id: 'fast', type: 'script', run: 'echo fast' },
+      ],
+    }
+
+    await manager.execute(slowAbility, {}, createMockContext())
+
+    const execution = await manager.execute(fastAbility, {}, createMockContext())
+    expect(execution.status).toBe('completed')
+  })
+
+  it('should cancel active execution', async () => {
+    const ability: Ability = {
+      name: 'cancel-test',
+      description: 'Cancel test',
+      steps: [
+        { id: 'step1', type: 'script', run: 'echo test' },
+      ],
+    }
+
+    const execution = await manager.execute(ability, {}, createMockContext())
+
+    expect(execution.status).toBe('completed')
+
+    const cancelled = manager.cancelActive()
+    expect(cancelled).toBe(false)
+  })
+
+  it('should cleanup old executions', async () => {
+    const ability: Ability = {
+      name: 'cleanup-test',
+      description: 'Cleanup test',
+      steps: [
+        { id: 'step1', type: 'script', run: 'echo test' },
+      ],
+    }
+
+    for (let i = 0; i < 60; i++) {
+      await manager.execute(
+        { ...ability, name: `cleanup-test-${i}` },
+        {},
+        createMockContext()
+      )
+    }
+
+    expect(manager.list().length).toBeLessThanOrEqual(50)
+  })
+})
+
+describe('Integration: Context Passing', () => {
+  it('should pass outputs between steps', async () => {
+    const ability: Ability = {
+      name: 'context-test',
+      description: 'Context passing test',
+      steps: [
+        {
+          id: 'generate',
+          type: 'script',
+          run: 'echo "GENERATED_VALUE_123"',
+        },
+        {
+          id: 'use',
+          type: 'script',
+          run: 'echo "Received: {{steps.generate.output}}"',
+          needs: ['generate'],
+        },
+      ],
+    }
+
+    const execution = await executeAbility(ability, {}, createMockContext())
+
+    expect(execution.status).toBe('completed')
+    expect(execution.completedSteps[1].output).toContain('GENERATED_VALUE_123')
+  })
+})
+
+describe('Integration: Error Handling', () => {
+  it('should handle script failures gracefully', async () => {
+    const ability: Ability = {
+      name: 'error-test',
+      description: 'Error handling test',
+      steps: [
+        {
+          id: 'fail',
+          type: 'script',
+          run: 'exit 1',
+          validation: { exit_code: 0 },
+        },
+      ],
+    }
+
+    const execution = await executeAbility(ability, {}, createMockContext())
+
+    expect(execution.status).toBe('failed')
+    expect(execution.error).toBeDefined()
+  })
+
+  it('should handle missing commands gracefully', async () => {
+    const ability: Ability = {
+      name: 'missing-cmd-test',
+      description: 'Missing command test',
+      steps: [
+        {
+          id: 'missing',
+          type: 'script',
+          run: 'nonexistent_command_12345',
+        },
+      ],
+    }
+
+    const execution = await executeAbility(ability, {}, createMockContext())
+
+    expect(execution.completedSteps[0]).toBeDefined()
+  })
+
+  it('should validate inputs before execution', async () => {
+    const ability: Ability = {
+      name: 'input-validation-test',
+      description: 'Input validation test',
+      inputs: {
+        required_field: {
+          type: 'string',
+          required: true,
+        },
+      },
+      steps: [
+        { id: 'step1', type: 'script', run: 'echo test' },
+      ],
+    }
+
+    const execution = await executeAbility(ability, {}, createMockContext())
+
+    expect(execution.status).toBe('failed')
+    expect(execution.error).toContain('Input validation failed')
+  })
+})

+ 131 - 0
packages/plugin-abilities/tests/sdk.test.ts

@@ -0,0 +1,131 @@
+import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
+import { AbilitiesSDK, createAbilitiesSDK } from '../src/sdk.js'
+import * as fs from 'fs'
+import * as path from 'path'
+import * as os from 'os'
+
+describe('AbilitiesSDK', () => {
+  let sdk: AbilitiesSDK
+  let tempDir: string
+
+  beforeEach(async () => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'abilities-sdk-test-'))
+
+    fs.writeFileSync(
+      path.join(tempDir, 'test-ability.yaml'),
+      `
+name: test-sdk
+description: Test SDK ability
+triggers:
+  keywords:
+    - test
+inputs:
+  message:
+    type: string
+    default: "Hello"
+steps:
+  - id: echo
+    type: script
+    run: echo "{{inputs.message}}"
+`
+    )
+
+    sdk = createAbilitiesSDK({ projectDir: tempDir, includeGlobal: false })
+  })
+
+  afterEach(() => {
+    sdk.cleanup()
+    fs.rmSync(tempDir, { recursive: true, force: true })
+  })
+
+  test('should list loaded abilities', async () => {
+    const abilities = await sdk.list()
+
+    expect(abilities.length).toBe(1)
+    expect(abilities[0].name).toBe('test-sdk')
+    expect(abilities[0].description).toBe('Test SDK ability')
+    expect(abilities[0].stepCount).toBe(1)
+  })
+
+  test('should get ability by name', async () => {
+    const ability = await sdk.get('test-sdk')
+
+    expect(ability).toBeDefined()
+    expect(ability?.name).toBe('test-sdk')
+    expect(ability?.steps.length).toBe(1)
+  })
+
+  test('should return undefined for unknown ability', async () => {
+    const ability = await sdk.get('nonexistent')
+
+    expect(ability).toBeUndefined()
+  })
+
+  test('should validate ability', async () => {
+    const result = await sdk.validate('test-sdk')
+
+    expect(result.valid).toBe(true)
+    expect(result.errors.length).toBe(0)
+  })
+
+  test('should return error for unknown ability validation', async () => {
+    const result = await sdk.validate('nonexistent')
+
+    expect(result.valid).toBe(false)
+    expect(result.errors[0]).toContain('not found')
+  })
+
+  test('should execute ability', async () => {
+    const result = await sdk.execute('test-sdk', { message: 'World' })
+
+    expect(result.status).toBe('completed')
+    expect(result.ability).toBe('test-sdk')
+    expect(result.steps.length).toBe(1)
+    expect(result.steps[0].status).toBe('completed')
+    expect(result.error).toBeUndefined()
+  })
+
+  test('should execute ability with defaults', async () => {
+    const result = await sdk.execute('test-sdk')
+
+    expect(result.status).toBe('completed')
+  })
+
+  test('should return error for unknown ability execution', async () => {
+    const result = await sdk.execute('nonexistent')
+
+    expect(result.status).toBe('failed')
+    expect(result.error).toContain('not found')
+  })
+
+  test('should get status of active execution', async () => {
+    const status = await sdk.status()
+
+    expect(status.active).toBe(false)
+  })
+
+  test('should cancel execution', async () => {
+    const cancelled = await sdk.cancel()
+
+    expect(cancelled).toBe(false)
+  })
+})
+
+describe('createAbilitiesSDK', () => {
+  test('should create SDK instance', () => {
+    const sdk = createAbilitiesSDK()
+
+    expect(sdk).toBeInstanceOf(AbilitiesSDK)
+    sdk.cleanup()
+  })
+
+  test('should accept options', () => {
+    const sdk = createAbilitiesSDK({
+      projectDir: '/tmp/test',
+      includeGlobal: false,
+    })
+
+    expect(sdk).toBeInstanceOf(AbilitiesSDK)
+    sdk.cleanup()
+  })
+})

+ 131 - 0
packages/plugin-abilities/tests/trigger.test.ts

@@ -0,0 +1,131 @@
+import { describe, expect, it } from 'bun:test'
+import type { Ability, Triggers } from '../src/types/index.js'
+
+function matchesTrigger(text: string, triggers: Triggers | undefined): boolean {
+  if (!triggers) return false
+
+  const lowerText = text.toLowerCase()
+
+  if (triggers.keywords) {
+    for (const keyword of triggers.keywords) {
+      if (lowerText.includes(keyword.toLowerCase())) {
+        return true
+      }
+    }
+  }
+
+  if (triggers.patterns) {
+    for (const pattern of triggers.patterns) {
+      try {
+        if (new RegExp(pattern, 'i').test(text)) {
+          return true
+        }
+      } catch {
+        continue
+      }
+    }
+  }
+
+  return false
+}
+
+describe('Trigger Detection', () => {
+  describe('keyword matching', () => {
+    it('should match exact keyword', () => {
+      const triggers: Triggers = { keywords: ['deploy'] }
+      expect(matchesTrigger('deploy to production', triggers)).toBe(true)
+    })
+
+    it('should match keyword case-insensitively', () => {
+      const triggers: Triggers = { keywords: ['Deploy'] }
+      expect(matchesTrigger('DEPLOY now', triggers)).toBe(true)
+    })
+
+    it('should match keyword as substring', () => {
+      const triggers: Triggers = { keywords: ['ship'] }
+      expect(matchesTrigger('ship it!', triggers)).toBe(true)
+    })
+
+    it('should not match when keyword not present', () => {
+      const triggers: Triggers = { keywords: ['deploy'] }
+      expect(matchesTrigger('release the code', triggers)).toBe(false)
+    })
+
+    it('should match any of multiple keywords', () => {
+      const triggers: Triggers = { keywords: ['deploy', 'release', 'ship'] }
+      expect(matchesTrigger('release v1.0', triggers)).toBe(true)
+      expect(matchesTrigger('ship it', triggers)).toBe(true)
+    })
+  })
+
+  describe('pattern matching', () => {
+    it('should match regex pattern', () => {
+      const triggers: Triggers = { patterns: ['deploy.*prod'] }
+      expect(matchesTrigger('deploy to production', triggers)).toBe(true)
+    })
+
+    it('should match version pattern', () => {
+      const triggers: Triggers = { patterns: ['v\\d+\\.\\d+\\.\\d+'] }
+      expect(matchesTrigger('release v1.2.3', triggers)).toBe(true)
+    })
+
+    it('should not match invalid pattern gracefully', () => {
+      const triggers: Triggers = { patterns: ['[invalid(regex'] }
+      expect(matchesTrigger('some text', triggers)).toBe(false)
+    })
+
+    it('should match case-insensitively', () => {
+      const triggers: Triggers = { patterns: ['DEPLOY'] }
+      expect(matchesTrigger('deploy now', triggers)).toBe(true)
+    })
+  })
+
+  describe('combined triggers', () => {
+    it('should match keyword OR pattern', () => {
+      const triggers: Triggers = {
+        keywords: ['ship it'],
+        patterns: ['deploy.*prod']
+      }
+      expect(matchesTrigger('ship it now', triggers)).toBe(true)
+      expect(matchesTrigger('deploy to prod', triggers)).toBe(true)
+    })
+
+    it('should return false when no triggers defined', () => {
+      expect(matchesTrigger('anything', undefined)).toBe(false)
+      expect(matchesTrigger('anything', {})).toBe(false)
+    })
+  })
+
+  describe('ability trigger detection', () => {
+    it('should detect ability from user message', () => {
+      const abilities: Ability[] = [
+        {
+          name: 'deploy',
+          description: 'Deploy to production',
+          triggers: { keywords: ['deploy', 'ship it'] },
+          steps: [{ id: 'test', type: 'script', run: 'echo test' }]
+        },
+        {
+          name: 'test',
+          description: 'Run tests',
+          triggers: { keywords: ['run tests', 'test suite'] },
+          steps: [{ id: 'test', type: 'script', run: 'npm test' }]
+        }
+      ]
+
+      const detectAbility = (text: string): Ability | null => {
+        for (const ability of abilities) {
+          if (matchesTrigger(text, ability.triggers)) {
+            return ability
+          }
+        }
+        return null
+      }
+
+      expect(detectAbility('please deploy to staging')?.name).toBe('deploy')
+      expect(detectAbility('ship it!')?.name).toBe('deploy')
+      expect(detectAbility('run tests please')?.name).toBe('test')
+      expect(detectAbility('check the code')).toBe(null)
+    })
+  })
+})

+ 174 - 0
packages/plugin-abilities/tests/validator.test.ts

@@ -0,0 +1,174 @@
+import { describe, expect, it } from 'bun:test'
+import { validateAbility, validateInputs } from '../src/validator/index.js'
+import type { Ability } from '../src/types/index.js'
+
+describe('validateAbility', () => {
+  it('should validate a minimal valid ability', () => {
+    const ability = {
+      name: 'test-ability',
+      description: 'A test ability',
+      steps: [
+        {
+          id: 'step1',
+          type: 'script',
+          run: 'echo hello',
+        },
+      ],
+    }
+
+    const result = validateAbility(ability)
+    expect(result.valid).toBe(true)
+    expect(result.errors).toHaveLength(0)
+  })
+
+  it('should reject ability without name', () => {
+    const ability = {
+      description: 'A test ability',
+      steps: [{ id: 'step1', type: 'script', run: 'echo' }],
+    }
+
+    const result = validateAbility(ability)
+    expect(result.valid).toBe(false)
+    expect(result.errors.some((e) => e.path.includes('name'))).toBe(true)
+  })
+
+  it('should reject ability without steps', () => {
+    const ability = {
+      name: 'test',
+      description: 'Test',
+      steps: [],
+    }
+
+    const result = validateAbility(ability)
+    expect(result.valid).toBe(false)
+  })
+
+  it('should detect circular dependencies', () => {
+    const ability = {
+      name: 'circular',
+      description: 'Has circular deps',
+      steps: [
+        { id: 'a', type: 'script', run: 'echo a', needs: ['c'] },
+        { id: 'b', type: 'script', run: 'echo b', needs: ['a'] },
+        { id: 'c', type: 'script', run: 'echo c', needs: ['b'] },
+      ],
+    }
+
+    const result = validateAbility(ability)
+    expect(result.valid).toBe(false)
+    expect(result.errors.some((e) => e.code === 'CIRCULAR_DEPENDENCY')).toBe(true)
+  })
+
+  it('should detect missing dependencies', () => {
+    const ability = {
+      name: 'missing-dep',
+      description: 'Missing dep',
+      steps: [
+        { id: 'a', type: 'script', run: 'echo', needs: ['nonexistent'] },
+      ],
+    }
+
+    const result = validateAbility(ability)
+    expect(result.valid).toBe(false)
+    expect(result.errors.some((e) => e.code === 'MISSING_DEPENDENCY')).toBe(true)
+  })
+
+  it('should detect duplicate step IDs', () => {
+    const ability = {
+      name: 'duplicate',
+      description: 'Duplicate IDs',
+      steps: [
+        { id: 'step1', type: 'script', run: 'echo 1' },
+        { id: 'step1', type: 'script', run: 'echo 2' },
+      ],
+    }
+
+    const result = validateAbility(ability)
+    expect(result.valid).toBe(false)
+    expect(result.errors.some((e) => e.code === 'DUPLICATE_ID')).toBe(true)
+  })
+
+  it('should validate all step types', () => {
+    const ability = {
+      name: 'all-types',
+      description: 'All step types',
+      steps: [
+        { id: 'script', type: 'script', run: 'echo' },
+        { id: 'agent', type: 'agent', agent: 'oracle', prompt: 'Help' },
+        { id: 'skill', type: 'skill', skill: 'test-skill' },
+        { id: 'approval', type: 'approval', prompt: 'Approve?' },
+        { id: 'workflow', type: 'workflow', workflow: 'sub-workflow' },
+      ],
+    }
+
+    const result = validateAbility(ability)
+    expect(result.valid).toBe(true)
+  })
+})
+
+describe('validateInputs', () => {
+  const ability: Ability = {
+    name: 'test',
+    description: 'Test',
+    inputs: {
+      version: {
+        type: 'string',
+        required: true,
+        pattern: '^v\\d+\\.\\d+\\.\\d+$',
+      },
+      count: {
+        type: 'number',
+        required: false,
+        min: 1,
+        max: 100,
+      },
+      env: {
+        type: 'string',
+        required: true,
+        enum: ['dev', 'staging', 'prod'],
+      },
+    },
+    steps: [{ id: 'test', type: 'script', run: 'echo' }],
+  }
+
+  it('should validate correct inputs', () => {
+    const errors = validateInputs(ability, {
+      version: 'v1.2.3',
+      count: 50,
+      env: 'staging',
+    })
+    expect(errors).toHaveLength(0)
+  })
+
+  it('should reject missing required input', () => {
+    const errors = validateInputs(ability, {
+      env: 'dev',
+    })
+    expect(errors.some((e) => e.code === 'MISSING_INPUT')).toBe(true)
+  })
+
+  it('should reject invalid pattern', () => {
+    const errors = validateInputs(ability, {
+      version: 'invalid',
+      env: 'dev',
+    })
+    expect(errors.some((e) => e.code === 'PATTERN_MISMATCH')).toBe(true)
+  })
+
+  it('should reject invalid enum value', () => {
+    const errors = validateInputs(ability, {
+      version: 'v1.0.0',
+      env: 'invalid',
+    })
+    expect(errors.some((e) => e.code === 'ENUM_MISMATCH')).toBe(true)
+  })
+
+  it('should reject number out of range', () => {
+    const errors = validateInputs(ability, {
+      version: 'v1.0.0',
+      env: 'dev',
+      count: 200,
+    })
+    expect(errors.some((e) => e.code === 'MAX_VALUE')).toBe(true)
+  })
+})

+ 23 - 0
packages/plugin-abilities/tsconfig.json

@@ -0,0 +1,23 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": false,
+    "lib": ["ES2022"],
+    "types": ["node", "bun"]
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist", "tests"]
+}

+ 347 - 1
registry.json

@@ -398,6 +398,21 @@
         ],
         "category": "core"
       },
+      {
+        "id": "context-retriever",
+        "name": "Context Retriever",
+        "type": "subagent",
+        "path": ".opencode/agent/subagents/core/context-retriever.md",
+        "description": "Generic context search and retrieval specialist for finding relevant context files, standards, and guides in any repository",
+        "tags": [
+          "context",
+          "search",
+          "retrieval",
+          "subagent"
+        ],
+        "dependencies": [],
+        "category": "core"
+      },
       {
         "id": "simple-responder",
         "name": "Simple Responder",
@@ -1936,6 +1951,296 @@
         ],
         "dependencies": [],
         "category": "standard"
+      },
+      {
+        "id": "task-commands",
+        "name": "Task Commands",
+        "type": "context",
+        "path": ".opencode/context/core/task-management/lookup/task-commands.md",
+        "description": "Lookup: Task CLI Commands",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "navigation",
+        "name": "Navigation",
+        "type": "context",
+        "path": ".opencode/context/core/task-management/navigation.md",
+        "description": "Task Management Navigation",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "splitting-tasks",
+        "name": "Splitting Tasks",
+        "type": "context",
+        "path": ".opencode/context/core/task-management/guides/splitting-tasks.md",
+        "description": "Guide: Splitting Features into Tasks",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "managing-tasks",
+        "name": "Managing Tasks",
+        "type": "context",
+        "path": ".opencode/context/core/task-management/guides/managing-tasks.md",
+        "description": "Guide: Managing Task Lifecycle",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "task-schema",
+        "name": "Task Schema",
+        "type": "context",
+        "path": ".opencode/context/core/task-management/standards/task-schema.md",
+        "description": "Standard: Task JSON Schema",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "design-assets",
+        "name": "Design Assets",
+        "type": "context",
+        "path": ".opencode/context/development/design-assets.md",
+        "description": "Design Assets",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "ui-styling-standards",
+        "name": "Ui Styling Standards",
+        "type": "context",
+        "path": ".opencode/context/development/ui-styling-standards.md",
+        "description": "UI Styling Standards",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "clean-code",
+        "name": "Clean Code",
+        "type": "context",
+        "path": ".opencode/context/development/clean-code.md",
+        "description": "Clean Code Principles",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "animation-patterns",
+        "name": "Animation Patterns",
+        "type": "context",
+        "path": ".opencode/context/development/animation-patterns.md",
+        "description": "Animation Patterns",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "design-systems",
+        "name": "Design Systems",
+        "type": "context",
+        "path": ".opencode/context/development/design-systems.md",
+        "description": "Design Systems",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "react-patterns",
+        "name": "React Patterns",
+        "type": "context",
+        "path": ".opencode/context/development/react-patterns.md",
+        "description": "React Patterns & Best Practices",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "api-design",
+        "name": "Api Design",
+        "type": "context",
+        "path": ".opencode/context/development/api-design.md",
+        "description": "API Design Patterns",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "mastra-config",
+        "name": "Mastra Config",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/lookup/mastra-config.md",
+        "description": "Lookup: Mastra Configuration",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "navigation",
+        "name": "Navigation",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/navigation.md",
+        "description": "MAStra AI Navigation",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "modular-building",
+        "name": "Modular Building",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/guides/modular-building.md",
+        "description": "Guide: Modular Mastra Building",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "testing",
+        "name": "Testing",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/guides/testing.md",
+        "description": "Guide: Testing Mastra",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "workflow-step-structure",
+        "name": "Workflow Step Structure",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/guides/workflow-step-structure.md",
+        "description": "Guide: Workflow Step Structure",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "workflow-example",
+        "name": "Workflow Example",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/examples/workflow-example.md",
+        "description": "Example: Document Ingestion Workflow",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "storage",
+        "name": "Storage",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/concepts/storage.md",
+        "description": "Concept: Mastra Data Storage",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "workflows",
+        "name": "Workflows",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/concepts/workflows.md",
+        "description": "Concept: Mastra Workflows",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "agents-tools",
+        "name": "Agents Tools",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/concepts/agents-tools.md",
+        "description": "Concept: Mastra Agents & Tools",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "core",
+        "name": "Core",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/concepts/core.md",
+        "description": "Concept: Mastra Core",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "evaluations",
+        "name": "Evaluations",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/concepts/evaluations.md",
+        "description": "Concept: Mastra Evaluations",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "mastra-errors",
+        "name": "Mastra Errors",
+        "type": "context",
+        "path": ".opencode/context/development/ai/mastra-ai/errors/mastra-errors.md",
+        "description": "Errors: Mastra Implementation",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "navigation",
+        "name": "Navigation",
+        "type": "context",
+        "path": ".opencode/context/development/ai/navigation.md",
+        "description": "AI Navigation",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "navigation",
+        "name": "Navigation",
+        "type": "context",
+        "path": ".opencode/context/development/frameworks/navigation.md",
+        "description": "Frameworks Navigation",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "navigation",
+        "name": "Navigation",
+        "type": "context",
+        "path": ".opencode/context/development/frameworks/tanstack-start/navigation.md",
+        "description": "Tanstack Start Navigation",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "copywriting-frameworks",
+        "name": "Copywriting Frameworks",
+        "type": "context",
+        "path": ".opencode/context/content/copywriting-frameworks.md",
+        "description": "Copywriting Frameworks",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
+      },
+      {
+        "id": "tone-voice",
+        "name": "Tone Voice",
+        "type": "context",
+        "path": ".opencode/context/content/tone-voice.md",
+        "description": "Tone & Voice Guidelines",
+        "tags": [],
+        "dependencies": [],
+        "category": "standard"
       }
     ],
     "config": [
@@ -1976,12 +2281,27 @@
       "components": [
         "agent:openagent",
         "subagent:task-manager",
+        "subagent:contextscout",
         "subagent:documentation",
+        "skill:task-management",
         "command:context",
         "command:clean",
         "tool:env",
         "context:essential-patterns",
         "context:project-context",
+        "context:quick-start",
+        "context:standards-code",
+        "context:standards-patterns",
+        "context:standards-tests",
+        "context:standards-docs",
+        "context:standards-analysis",
+        "context:workflows-delegation",
+        "context:workflows-sessions",
+        "context:workflows-task-breakdown",
+        "context:workflows-review",
+        "context:system-context-guide",
+        "context:adding-skill",
+        "context:adding-skill",
         "config:env-example"
       ]
     },
@@ -2003,6 +2323,8 @@
         "subagent:tester",
         "subagent:build-agent",
         "subagent:codebase-pattern-analyst",
+        "subagent:contextscout",
+        "skill:task-management",
         "command:commit",
         "command:test",
         "command:context",
@@ -2012,6 +2334,7 @@
         "tool:env",
         "context:essential-patterns",
         "context:project-context",
+        "context:quick-start",
         "context:standards-code",
         "context:standards-patterns",
         "context:standards-tests",
@@ -2022,6 +2345,7 @@
         "context:workflows-task-breakdown",
         "context:workflows-review",
         "context:system-context-guide",
+        "context:adding-skill",
         "config:env-example",
         "config:readme"
       ]
@@ -2035,8 +2359,10 @@
         "agent:technical-writer",
         "agent:data-analyst",
         "subagent:task-manager",
+        "subagent:contextscout",
         "subagent:documentation",
         "subagent:image-specialist",
+        "skill:task-management",
         "command:context",
         "command:clean",
         "command:prompt-enhancer",
@@ -2047,6 +2373,18 @@
         "plugin:telegram-notify",
         "context:essential-patterns",
         "context:project-context",
+        "context:quick-start",
+        "context:standards-code",
+        "context:standards-patterns",
+        "context:standards-tests",
+        "context:standards-docs",
+        "context:standards-analysis",
+        "context:workflows-delegation",
+        "context:workflows-sessions",
+        "context:workflows-task-breakdown",
+        "context:workflows-review",
+        "context:system-context-guide",
+        "context:adding-skill",
         "config:env-example",
         "config:readme"
       ]
@@ -2072,6 +2410,8 @@
         "subagent:tester",
         "subagent:build-agent",
         "subagent:codebase-pattern-analyst",
+        "subagent:contextscout",
+        "skill:task-management",
         "subagent:image-specialist",
         "command:test",
         "command:commit",
@@ -2088,6 +2428,7 @@
         "plugin:telegram-notify",
         "context:essential-patterns",
         "context:project-context",
+        "context:quick-start",
         "context:standards-code",
         "context:standards-patterns",
         "context:standards-tests",
@@ -2098,6 +2439,7 @@
         "context:workflows-task-breakdown",
         "context:workflows-review",
         "context:system-context-guide",
+        "context:adding-skill",
         "config:env-example",
         "config:readme"
       ]
@@ -2127,6 +2469,8 @@
         "subagent:codebase-pattern-analyst",
         "subagent:image-specialist",
         "subagent:context-retriever",
+        "subagent:contextscout",
+        "skill:task-management",
         "subagent:domain-analyzer",
         "subagent:agent-generator",
         "subagent:context-organizer",
@@ -2148,6 +2492,7 @@
         "plugin:telegram-notify",
         "context:essential-patterns",
         "context:project-context",
+        "context:quick-start",
         "context:standards-code",
         "context:standards-patterns",
         "context:standards-tests",
@@ -2158,6 +2503,7 @@
         "context:workflows-task-breakdown",
         "context:workflows-review",
         "context:system-context-guide",
+        "context:adding-skill",
         "config:env-example",
         "config:readme"
       ],
@@ -2168,7 +2514,7 @@
     }
   },
   "metadata": {
-    "lastUpdated": "2026-01-09",
+    "lastUpdated": "2026-01-11",
     "schemaVersion": "1.0.0"
   },
   "subagents": {

+ 10 - 5
scripts/auto-detect-components.sh

@@ -89,7 +89,8 @@ extract_metadata_from_file() {
     fi
     
     # Generate ID from filename
-    local filename=$(basename "$file" .md)
+    local filename
+    filename=$(basename "$file" .md)
     id=$(echo "$filename" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
     
     # Generate name from filename (capitalize words)
@@ -131,7 +132,8 @@ scan_for_new_components() {
     echo ""
     
     # Get all paths from registry
-    local registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
+    local registry_paths
+    registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
     
     # Scan .opencode directory
     local categories=("agent" "command" "tool" "plugin" "context")
@@ -160,11 +162,13 @@ scan_for_new_components() {
             # Check if this path is in registry
             if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
                 # Extract metadata
-                local metadata=$(extract_metadata_from_file "$file")
+                local metadata
+                metadata=$(extract_metadata_from_file "$file")
                 IFS='|' read -r id name description <<< "$metadata"
                 
                 # Detect component type
-                local comp_type=$(detect_component_type "$rel_path")
+                local comp_type
+                comp_type=$(detect_component_type "$rel_path")
                 
                 if [ "$comp_type" != "unknown" ]; then
                     NEW_COMPONENTS+=("${comp_type}|${id}|${name}|${description}|${rel_path}")
@@ -194,7 +198,8 @@ add_component_to_registry() {
     description=$(echo "$description" | sed 's/"/\\"/g' | sed "s/'/\\'/g")
     
     # Get registry key (agents, subagents, commands, etc.)
-    local registry_key=$(get_registry_key "$comp_type")
+    local registry_key
+    registry_key=$(get_registry_key "$comp_type")
     
     # Use jq to properly construct JSON (avoids escaping issues)
     local temp_file="${REGISTRY_FILE}.tmp"

+ 8 - 4
scripts/check-context-logs/count-agent-tokens.sh

@@ -47,8 +47,10 @@ count_tokens() {
         return 0
     fi
     
-    local word_count=$(wc -w < "$file" 2>/dev/null || echo 0)
-    local token_estimate=$(echo "$word_count * $TOKEN_MULTIPLIER" | bc | cut -d. -f1)
+    local word_count
+    word_count=$(wc -w < "$file" 2>/dev/null || echo 0)
+    local token_estimate
+    token_estimate=$(echo "$word_count * $TOKEN_MULTIPLIER" | bc | cut -d. -f1)
     
     echo -e "${GREEN}✓${NC} ${label}"
     echo -e "  ${YELLOW}→${NC} $file"
@@ -67,8 +69,10 @@ count_tokens_from_output() {
         return 0
     fi
     
-    local word_count=$(echo "$output" | wc -w)
-    local token_estimate=$(echo "$word_count * $TOKEN_MULTIPLIER" | bc | cut -d. -f1)
+    local word_count
+    word_count=$(echo "$output" | wc -w)
+    local token_estimate
+    token_estimate=$(echo "$word_count * $TOKEN_MULTIPLIER" | bc | cut -d. -f1)
     
     echo -e "${GREEN}✓${NC} ${label}"
     echo -e "  ${CYAN}~$token_estimate tokens${NC} ($word_count words)"

+ 2 - 2
scripts/check-context-logs/show-api-payload.sh

@@ -6,8 +6,8 @@
 
 set -euo pipefail
 
-AGENT="${1:-build}"
-SESSION_ID="${2:-demo}"
+# AGENT="${1:-build}" # Unused
+# SESSION_ID="${2:-demo}" # Unused
 
 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
 echo "  What Gets Sent to AI API (Anthropic/Claude)"

+ 2 - 2
scripts/development/demo.sh

@@ -10,11 +10,11 @@
 set -e
 
 # Colors
-RED='\033[0;31m'
+# RED='\033[0;31m' # Unused
 GREEN='\033[0;32m'
 YELLOW='\033[1;33m'
 BLUE='\033[0;34m'
-PURPLE='\033[0;35m'
+# PURPLE='\033[0;35m' # Unused
 CYAN='\033[0;36m'
 NC='\033[0m' # No Color
 

+ 2 - 1
scripts/prompts/test-prompt.sh

@@ -174,7 +174,7 @@ else
         echo "Available variants for $AGENT_NAME:"
         echo "  - default (canonical agent file)"
         if [[ -d "$PROMPTS_DIR/$AGENT_NAME" ]]; then
-            ls -1 "$PROMPTS_DIR/$AGENT_NAME/"*.md 2>/dev/null | grep -v "TEMPLATE.md" | grep -v "README.md" | xargs -I {} basename {} .md || echo "  (no model variants found)"
+            find "$PROMPTS_DIR/$AGENT_NAME" -maxdepth 1 -name "*.md" -not -name "TEMPLATE.md" -not -name "README.md" -exec basename {} .md \; || echo "  (no model variants found)"
         fi
         exit 1
     fi
@@ -264,6 +264,7 @@ cd "$EVALS_DIR"
 set +e  # Don't exit on test failure
 npm run eval:sdk:core -- --agent="$AGENT_NAME" --model="$MODEL"
 TEST_EXIT_CODE=$?
+export TEST_EXIT_CODE
 set -e
 
 echo ""

+ 2 - 2
scripts/prompts/use-prompt.sh

@@ -155,7 +155,7 @@ if [[ ! -f "$AGENT_FILE" ]]; then
     echo -e "${RED}Error: Agent not found: $AGENT_FILE${NC}"
     echo ""
     echo "Available agents:"
-    ls -1 "$AGENT_DIR/"*.md 2>/dev/null | xargs -I {} basename {} .md || echo "  (none found)"
+    find "$AGENT_DIR" -maxdepth 1 -name "*.md" -exec basename {} .md \; || echo "  (none found)"
     exit 1
 fi
 
@@ -181,7 +181,7 @@ if [[ ! -f "$PROMPT_FILE" ]]; then
     echo "Available variants for $AGENT_NAME:"
     echo "  - default (canonical agent file)"
     if [[ -d "$PROMPTS_DIR/$AGENT_NAME" ]]; then
-        ls -1 "$PROMPTS_DIR/$AGENT_NAME/"*.md 2>/dev/null | grep -v "TEMPLATE.md" | grep -v "README.md" | xargs -I {} basename {} .md || echo "  (no model variants found)"
+        find "$PROMPTS_DIR/$AGENT_NAME" -maxdepth 1 -name "*.md" -not -name "TEMPLATE.md" -not -name "README.md" -exec basename {} .md \; || echo "  (no model variants found)"
     else
         echo "  (no model variants found)"
     fi

+ 47 - 24
scripts/registry/auto-detect-components.sh

@@ -189,18 +189,23 @@ find_similar_path() {
     local wrong_path=$1
     
     # Get directory and filename
-    local dir=$(dirname "$wrong_path")
-    local filename=$(basename "$wrong_path")
+    local dir
+    dir=$(dirname "$wrong_path")
+    local filename
+    filename=$(basename "$wrong_path")
     
     # First, try to find exact filename match in category subdirectories
     # e.g., .opencode/agent/opencoder.md → .opencode/agent/core/opencoder.md
-    local base_dir=$(echo "$dir" | cut -d'/' -f1-2)  # e.g., .opencode/agent
+    local base_dir
+    base_dir=$(echo "$dir" | cut -d'/' -f1-2)  # e.g., .opencode/agent
     
     if [ -d "$REPO_ROOT/$base_dir" ]; then
         # Search recursively in the base directory for exact filename match
         while IFS= read -r candidate; do
-            local candidate_rel="${candidate#$REPO_ROOT/}"
-            local candidate_name=$(basename "$candidate")
+            local candidate_rel
+            candidate_rel="${candidate#$REPO_ROOT/}"
+            local candidate_name
+            candidate_name=$(basename "$candidate")
             
             # Exact filename match
             if [[ "$candidate_name" == "$filename" ]]; then
@@ -220,8 +225,10 @@ find_similar_path() {
         
         # Find files with similar names
         while IFS= read -r candidate; do
-            local candidate_rel="${candidate#$REPO_ROOT/}"
-            local candidate_name=$(basename "$candidate")
+            local candidate_rel
+            candidate_rel="${candidate#$REPO_ROOT/}"
+            local candidate_name
+            candidate_name=$(basename "$candidate")
             
             # Simple similarity check
             if [[ "$candidate_name" == *"$filename"* ]] || [[ "$filename" == *"$candidate_name"* ]]; then
@@ -243,16 +250,21 @@ validate_existing_entries() {
     echo ""
     
     # Get all component types from registry
-    local component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
+    local component_types
+    component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
     
     while IFS= read -r comp_type; do
         # Get all components of this type
-        local count=$(jq -r ".components.${comp_type} | length" "$REGISTRY_FILE" 2>/dev/null)
+        local count
+        count=$(jq -r ".components.${comp_type} | length" "$REGISTRY_FILE" 2>/dev/null)
         
         for ((i=0; i<count; i++)); do
-            local id=$(jq -r ".components.${comp_type}[$i].id" "$REGISTRY_FILE" 2>/dev/null)
-            local name=$(jq -r ".components.${comp_type}[$i].name" "$REGISTRY_FILE" 2>/dev/null)
-            local path=$(jq -r ".components.${comp_type}[$i].path" "$REGISTRY_FILE" 2>/dev/null)
+            local id
+            id=$(jq -r ".components.${comp_type}[$i].id" "$REGISTRY_FILE" 2>/dev/null)
+            local name
+            name=$(jq -r ".components.${comp_type}[$i].name" "$REGISTRY_FILE" 2>/dev/null)
+            local path
+            path=$(jq -r ".components.${comp_type}[$i].path" "$REGISTRY_FILE" 2>/dev/null)
             
             # Skip if path is null or empty
             if [ -z "$path" ] || [ "$path" = "null" ]; then
@@ -362,7 +374,8 @@ extract_metadata_from_file() {
         # Extract tags from frontmatter (YAML array format)
         # Handles both: tags: [tag1, tag2] and multi-line format
         local in_tags=false
-        local frontmatter=$(sed -n '/^---$/,/^---$/p' "$file")
+        local frontmatter
+        frontmatter=$(sed -n '/^---$/,/^---$/p' "$file")
         while IFS= read -r line; do
             if [[ "$line" =~ ^tags: ]]; then
                 # Inline array format: tags: [tag1, tag2]
@@ -377,7 +390,8 @@ extract_metadata_from_file() {
                     # End of frontmatter
                     in_tags=false
                 elif [[ "$line" =~ ^[[:space:]]*- ]]; then
-                    local tag=$(echo "$line" | sed 's/^[[:space:]]*- *//')
+                    local tag
+                    tag=$(echo "$line" | sed 's/^[[:space:]]*- *//')
                     if [ -z "$tags" ]; then
                         tags="$tag"
                     else
@@ -407,7 +421,8 @@ extract_metadata_from_file() {
                     in_deps=false
                 elif [[ "$line" =~ ^[[:space:]]*- ]]; then
                     # Extract dependency (remove leading dash and whitespace)
-                    local dep=$(echo "$line" | sed 's/^[[:space:]]*- *//' | sed 's/"//g; s/'"'"'//g' | sed 's/#.*//' | xargs)
+                    local dep
+                    dep=$(echo "$line" | sed 's/^[[:space:]]*- *//' | sed 's/"//g; s/'"'"'//g' | sed 's/#.*//' | xargs)
                     # Skip empty lines and comments
                     if [ -n "$dep" ] && [[ ! "$dep" =~ ^# ]]; then
                         if [ -z "$dependencies" ]; then
@@ -433,7 +448,8 @@ extract_metadata_from_file() {
     fi
     
     # Generate ID from filename
-    local filename=$(basename "$file" .md)
+    local filename
+    filename=$(basename "$file" .md)
     id=$(echo "$filename" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
     
     # Generate name from filename (capitalize words)
@@ -475,7 +491,8 @@ extract_category_from_path() {
     elif [[ "$path" == *"/agent/"* ]]; then
         # For agents: .opencode/agent/core/openagent.md → core
         # Check if there's a category subdirectory
-        local category=$(echo "$path" | sed -E 's|.*/agent/([^/]+)/.*|\1|')
+        local category
+        category=$(echo "$path" | sed -E 's|.*/agent/([^/]+)/.*|\1|')
         # If category is the filename, it's a flat structure (no category)
         if [[ "$category" == *.md ]]; then
             echo "standard"
@@ -500,7 +517,8 @@ scan_for_new_components() {
     echo ""
     
     # Get all paths from registry
-    local registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
+    local registry_paths
+    registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
     
     # Scan .opencode directory
     local categories=("agent" "command" "tool" "plugin" "context")
@@ -534,14 +552,17 @@ scan_for_new_components() {
             # Check if this path is in registry
             if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
                 # Extract metadata
-                local metadata=$(extract_metadata_from_file "$file")
+                local metadata
+                metadata=$(extract_metadata_from_file "$file")
                 IFS='|' read -r id name description tags dependencies <<< "$metadata"
                 
                 # Detect component type
-                local comp_type=$(detect_component_type "$rel_path")
+                local comp_type
+                comp_type=$(detect_component_type "$rel_path")
                 
                 # Extract category from path
-                local comp_category=$(extract_category_from_path "$rel_path")
+                local comp_category
+                comp_category=$(extract_category_from_path "$rel_path")
                 
                 if [ "$comp_type" != "unknown" ]; then
                     NEW_COMPONENTS+=("${comp_type}|${id}|${name}|${description}|${rel_path}|${comp_category}|${tags}|${dependencies}")
@@ -566,7 +587,7 @@ scan_for_new_components() {
 
 check_dependencies() {
     local deps_str=$1
-    local component_name=$2
+    # local component_name=$2 # Unused
     
     if [ -z "$deps_str" ]; then
         return 0
@@ -607,7 +628,8 @@ check_dependencies() {
         esac
         
         # Check if exists in registry
-        local exists=$(jq -r ".components.${category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
+        local exists
+        exists=$(jq -r ".components.${category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
         
         if [ -z "$exists" ]; then
             echo "    ⚠ Dependency not found in registry: ${dep}"
@@ -645,7 +667,8 @@ add_component_to_registry() {
     fi
     
     # Get registry key (agents, subagents, commands, etc.)
-    local registry_key=$(get_registry_key "$comp_type")
+    local registry_key
+    registry_key=$(get_registry_key "$comp_type")
     
     # Use jq to properly construct JSON (avoids escaping issues)
     local temp_file="${REGISTRY_FILE}.tmp"

+ 2 - 1
scripts/registry/validate-component.sh

@@ -34,7 +34,8 @@ validate_markdown_frontmatter() {
     fi
     
     # Extract frontmatter
-    local frontmatter=$(awk '/^---$/{if(++n==2)exit;next}n==1' "$file")
+    local frontmatter
+    frontmatter=$(awk '/^---$/{if(++n==2)exit;next}n==1' "$file")
     
     # Check for description
     if ! echo "$frontmatter" | grep -q "^description:"; then

+ 0 - 1
scripts/registry/validate-profile-coverage.sh

@@ -16,7 +16,6 @@ for agent in $agents; do
   category=$(cat registry.json | jq -r ".components.agents[] | select(.id == \"$agent\") | .category")
   
   # Check which profiles include this agent
-  in_essential=$(cat registry.json | jq -r ".profiles.essential.components[] | select(. == \"agent:$agent\")" 2>/dev/null || echo "")
   in_developer=$(cat registry.json | jq -r ".profiles.developer.components[] | select(. == \"agent:$agent\")" 2>/dev/null || echo "")
   in_business=$(cat registry.json | jq -r ".profiles.business.components[] | select(. == \"agent:$agent\")" 2>/dev/null || echo "")
   in_full=$(cat registry.json | jq -r ".profiles.full.components[] | select(. == \"agent:$agent\")" 2>/dev/null || echo "")

+ 61 - 17
scripts/registry/validate-registry.sh

@@ -130,7 +130,8 @@ validate_component_paths() {
     [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Checking ${category_display}...${NC}"
     
     # Get all components in this category
-    local components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
+    local components
+    components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
     
     if [ -z "$components" ]; then
         [ "$VERBOSE" = true ] && print_info "No ${category_display} found in registry"
@@ -138,9 +139,12 @@ validate_component_paths() {
     fi
     
     while IFS= read -r component; do
-        local id=$(echo "$component" | jq -r '.id')
-        local path=$(echo "$component" | jq -r '.path')
-        local name=$(echo "$component" | jq -r '.name')
+        local id
+        id=$(echo "$component" | jq -r '.id')
+        local path
+        path=$(echo "$component" | jq -r '.path')
+        local name
+        name=$(echo "$component" | jq -r '.name')
         
         TOTAL_PATHS=$((TOTAL_PATHS + 1))
         
@@ -166,12 +170,15 @@ suggest_fix() {
     local component_id=$2
     
     # Extract directory and filename
-    local dir=$(dirname "$missing_path")
-    local filename=$(basename "$missing_path")
-    local base_dir=$(echo "$dir" | cut -d'/' -f1-3)  # e.g., .opencode/command
+    local dir
+    dir=$(dirname "$missing_path")
+    # local filename=$(basename "$missing_path") # Unused
+    local base_dir
+    base_dir=$(echo "$dir" | cut -d'/' -f1-3)  # e.g., .opencode/command
     
     # Look for similar files in the expected directory and subdirectories
-    local similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
+    local similar_files
+    similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
     
     if [ -n "$similar_files" ]; then
         echo -e "  ${YELLOW}→ Possible matches:${NC}"
@@ -186,7 +193,8 @@ scan_for_orphaned_files() {
     [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}"
     
     # Get all paths from registry
-    local registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
+    local registry_paths
+    registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
     
     # Scan .opencode directory for markdown files
     local categories=("agent" "command" "tool" "plugin" "context")
@@ -207,6 +215,34 @@ scan_for_orphaned_files() {
                 continue
             fi
             
+            # Skip README files
+            if [[ "$rel_path" == *"README.md" ]]; then
+                continue
+            fi
+            
+            # Skip template files
+            if [[ "$rel_path" == *"-template.md" ]]; then
+                continue
+            fi
+            
+            # Skip tool/plugin TypeScript files
+            if [[ "$rel_path" == *"/tool/index.ts" ]] || [[ "$rel_path" == *"/tool/template/index.ts" ]]; then
+                continue
+            fi
+            if [[ "$rel_path" == *"/plugin/agent-validator.ts" ]]; then
+                continue
+            fi
+            
+            # Skip plugin internal docs and tests
+            if [[ "$rel_path" == *"/plugin/docs/"* ]] || [[ "$rel_path" == *"/plugin/tests/"* ]]; then
+                continue
+            fi
+            
+            # Skip scripts directories (internal CLI tools, not registry components)
+            if [[ "$rel_path" == *"/scripts/"* ]]; then
+                continue
+            fi
+            
             # Check if this path is in registry
             if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
                 ORPHANED_FILES=$((ORPHANED_FILES + 1))
@@ -265,7 +301,8 @@ check_dependency_exists() {
     
     # Check if component exists in registry
     # First try exact ID match
-    local exists=$(jq -r ".components.${registry_category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
+    local exists
+    exists=$(jq -r ".components.${registry_category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
     
     if [ -n "$exists" ]; then
         echo "found"
@@ -276,7 +313,8 @@ check_dependency_exists() {
     # Format: context:core/standards/code -> .opencode/context/core/standards/code.md
     if [ "$dep_type" = "context" ]; then
         local context_path=".opencode/context/${dep_id}.md"
-        local exists_by_path=$(jq -r ".components.${registry_category}[]? | select(.path == \"${context_path}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
+        local exists_by_path
+        exists_by_path=$(jq -r ".components.${registry_category}[]? | select(.path == \"${context_path}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
         
         if [ -n "$exists_by_path" ]; then
             echo "found"
@@ -294,20 +332,25 @@ validate_component_dependencies() {
     echo ""
     
     # Get all component types
-    local component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
+    local component_types
+    component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
     
     while IFS= read -r comp_type; do
         # Get all components of this type
-        local components=$(jq -r ".components.${comp_type}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
+        local components
+        components=$(jq -r ".components.${comp_type}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
         
         if [ -z "$components" ]; then
             continue
         fi
         
         while IFS= read -r component; do
-            local id=$(echo "$component" | jq -r '.id')
-            local name=$(echo "$component" | jq -r '.name')
-            local dependencies=$(echo "$component" | jq -r '.dependencies[]?' 2>/dev/null)
+            local id
+            id=$(echo "$component" | jq -r '.id')
+            local name
+            name=$(echo "$component" | jq -r '.name')
+            local dependencies
+            dependencies=$(echo "$component" | jq -r '.dependencies[]?' 2>/dev/null)
             
             if [ -z "$dependencies" ]; then
                 continue
@@ -319,7 +362,8 @@ validate_component_dependencies() {
                     continue
                 fi
                 
-                local result=$(check_dependency_exists "$dep")
+                local result
+                result=$(check_dependency_exists "$dep")
                 
                 case "$result" in
                     found)

+ 33 - 2
scripts/testing/test.sh

@@ -19,14 +19,45 @@ NC='\033[0m' # No Color
 AGENT=${1:-all}
 MODEL=${2:-opencode/grok-code-fast}
 shift 2 2>/dev/null || true
-EXTRA_ARGS="$@"
+EXTRA_ARGS=("$@")
 
 # Check if --core flag is present
 CORE_MODE=false
-if [[ "$AGENT" == "--core" ]] || [[ "$MODEL" == "--core" ]] || [[ "$EXTRA_ARGS" == *"--core"* ]]; then
+if [[ "$AGENT" == "--core" ]] || [[ "$MODEL" == "--core" ]] || [[ "${EXTRA_ARGS[*]}" == *"--core"* ]]; then
   CORE_MODE=true
 fi
 
+echo -e "${BLUE}🧪 OpenCode Agents Test Runner${NC}"
+echo -e "${BLUE}================================${NC}"
+echo ""
+if [ "$CORE_MODE" = true ]; then
+  echo -e "Mode:   ${YELLOW}CORE TEST SUITE (7 tests, ~5-8 min)${NC}"
+fi
+echo -e "Agent:  ${GREEN}${AGENT}${NC}"
+echo -e "Model:  ${GREEN}${MODEL}${NC}"
+if [ -n "${EXTRA_ARGS[*]}" ]; then
+  echo -e "Extra:  ${YELLOW}${EXTRA_ARGS[*]}${NC}"
+fi
+
+# Navigate to framework directory
+cd "$(dirname "$0")/../../evals/framework" || exit 1
+
+# Check if dependencies are installed
+if [ ! -d "node_modules" ]; then
+  echo -e "${YELLOW}⚠️  Dependencies not installed. Running npm install...${NC}"
+  npm install
+  echo ""
+fi
+
+# Run tests
+if [ "$AGENT" = "all" ]; then
+  echo -e "${YELLOW}Running tests for ALL agents...${NC}"
+  npm run eval:sdk -- --model="$MODEL" "${EXTRA_ARGS[@]}"
+else
+  echo -e "${YELLOW}Running tests for ${AGENT}...${NC}"
+  npm run eval:sdk -- --agent="$AGENT" --model="$MODEL" "${EXTRA_ARGS[@]}"
+fi
+
 echo -e "${BLUE}🧪 OpenCode Agents Test Runner${NC}"
 echo -e "${BLUE}================================${NC}"
 echo ""

+ 2 - 1
scripts/tests/test-collision-detection.sh

@@ -228,7 +228,8 @@ test_backup_strategy() {
     echo "original content" > .opencode/agent/test.md
     
     # Simulate backup
-    local backup_dir=".opencode.backup.$(date +%Y%m%d-%H%M%S)"
+    local backup_dir
+    backup_dir=".opencode.backup.$(date +%Y%m%d-%H%M%S)"
     local file=".opencode/agent/test.md"
     local backup_file="${backup_dir}/${file}"
     

+ 16 - 9
scripts/validate-registry.sh

@@ -128,7 +128,8 @@ validate_component_paths() {
     [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Checking ${category_display}...${NC}"
     
     # Get all components in this category
-    local components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
+    local components
+    components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
     
     if [ -z "$components" ]; then
         [ "$VERBOSE" = true ] && print_info "No ${category_display} found in registry"
@@ -136,9 +137,12 @@ validate_component_paths() {
     fi
     
     while IFS= read -r component; do
-        local id=$(echo "$component" | jq -r '.id')
-        local path=$(echo "$component" | jq -r '.path')
-        local name=$(echo "$component" | jq -r '.name')
+        local id
+        local path
+        local name
+        id=$(echo "$component" | jq -r '.id')
+        path=$(echo "$component" | jq -r '.path')
+        name=$(echo "$component" | jq -r '.name')
         
         TOTAL_PATHS=$((TOTAL_PATHS + 1))
         
@@ -164,12 +168,14 @@ suggest_fix() {
     local component_id=$2
     
     # Extract directory and filename
-    local dir=$(dirname "$missing_path")
-    local filename=$(basename "$missing_path")
-    local base_dir=$(echo "$dir" | cut -d'/' -f1-3)  # e.g., .opencode/command
+    local dir
+    local base_dir
+    dir=$(dirname "$missing_path")
+    base_dir=$(echo "$dir" | cut -d'/' -f1-3)  # e.g., .opencode/command
     
     # Look for similar files in the expected directory and subdirectories
-    local similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
+    local similar_files
+    similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
     
     if [ -n "$similar_files" ]; then
         echo -e "  ${YELLOW}→ Possible matches:${NC}"
@@ -184,7 +190,8 @@ scan_for_orphaned_files() {
     [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}"
     
     # Get all paths from registry
-    local registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
+    local registry_paths
+    registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
     
     # Scan .opencode directory for markdown files
     local categories=("agent" "command" "tool" "plugin" "context")

+ 10 - 5
scripts/validation/validate-test-suites.sh

@@ -65,7 +65,8 @@ echo ""
 validate_suite() {
     local agent=$1
     local suite_file=$2
-    local suite_name=$(basename "$suite_file" .json)
+    local suite_name
+    suite_name=$(basename "$suite_file" .json)
     
     TOTAL_SUITES=$((TOTAL_SUITES + 1))
     
@@ -116,7 +117,8 @@ validate_suite() {
     done < <(jq -r '.tests[].path' "$suite_file")
     
     # 4. Check test count matches
-    local declared_count=$(jq -r '.totalTests' "$suite_file")
+    local declared_count
+    declared_count=$(jq -r '.totalTests' "$suite_file")
     if [[ "$test_count" -ne "$declared_count" ]]; then
         echo -e "  ${YELLOW}⚠️  Test count mismatch: found $test_count, declared $declared_count${NC}"
         suite_warnings=$((suite_warnings + 1))
@@ -129,10 +131,13 @@ validate_suite() {
             echo -e "     - $missing"
             
             # Suggest similar files
-            local dir=$(dirname "$missing")
-            local filename=$(basename "$missing")
+            local dir
+            dir=$(dirname "$missing")
+            local filename
+            filename=$(basename "$missing")
             if [[ -d "$tests_dir/$dir" ]]; then
-                local similar=$(find "$tests_dir/$dir" -name "*.yaml" -type f -exec basename {} \; | grep -i "$(echo $filename | cut -d'-' -f1)" | head -3)
+                local similar
+                similar=$(find "$tests_dir/$dir" -name "*.yaml" -type f -exec basename {} \; | grep -i "$(echo $filename | cut -d'-' -f1)" | head -3)
                 if [[ -n "$similar" ]]; then
                     echo -e "       ${YELLOW}Did you mean?${NC}"
                     echo "$similar" | sed 's/^/         - /'

+ 1 - 1
update.sh

@@ -9,7 +9,7 @@ set -e
 
 # Colors
 GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
+# YELLOW='\033[1;33m' # Unused
 BLUE='\033[0;34m'
 CYAN='\033[0;36m'
 BOLD='\033[1m'