Browse Source

chore(repo): delete dead packages/plugin-abilities and its CI job

The package had zero consumers in its entire git history: no package.json
depended on it, no import crossed its boundary, it was absent from the
published tarball, and it was never registered in .opencode/opencode.json.
It also did not compile (92 tsc errors) and failed 23 of its own tests,
which CI carried as a known-red non-blocking job.

Removes the package, its packages-checks job and summary arm, the two
detect-pr-changes assertions that existed only to assert that job existed,
and its pnpm-lock importer. Lowers the has-packages gate count from 3 to 2
to match the remaining jobs.

Recoverable from history if abilities are ever revived; nothing that worked
is lost.
darrenhinde 2 weeks ago
parent
commit
cce525599d
36 changed files with 2 additions and 7030 deletions
  1. 1 57
      .github/workflows/packages-checks.yml
  2. 0 855
      packages/plugin-abilities/ARCHITECTURE.md
  3. 0 180
      packages/plugin-abilities/MINIMAL_TEST.md
  4. 0 320
      packages/plugin-abilities/QUICK_REFERENCE.md
  5. 0 401
      packages/plugin-abilities/README.md
  6. 0 187
      packages/plugin-abilities/SIMPLIFICATION_SUMMARY.md
  7. 0 177
      packages/plugin-abilities/docs/GETTING_STARTED.md
  8. 0 72
      packages/plugin-abilities/examples/deploy/ability.yaml
  9. 0 44
      packages/plugin-abilities/examples/test-suite/ability.yaml
  10. 0 32
      packages/plugin-abilities/examples/test/ability.yaml
  11. 0 61
      packages/plugin-abilities/package.json
  12. 0 72
      packages/plugin-abilities/src/context/discovery.ts
  13. 0 35
      packages/plugin-abilities/src/context/types.ts
  14. 0 59
      packages/plugin-abilities/src/executor/execution-manager.ts
  15. 0 241
      packages/plugin-abilities/src/executor/index.ts
  16. 0 39
      packages/plugin-abilities/src/index.ts
  17. 0 164
      packages/plugin-abilities/src/loader/index.ts
  18. 0 227
      packages/plugin-abilities/src/opencode-plugin.ts
  19. 0 801
      packages/plugin-abilities/src/plugin.ts
  20. 0 259
      packages/plugin-abilities/src/sdk.ts
  21. 0 124
      packages/plugin-abilities/src/types/index.ts
  22. 0 356
      packages/plugin-abilities/src/validator/index.ts
  23. 0 45
      packages/plugin-abilities/src/validator/permissions.ts
  24. 0 82
      packages/plugin-abilities/test-minimal.ts
  25. 0 87
      packages/plugin-abilities/test-plugin.ts
  26. 0 46
      packages/plugin-abilities/test-run-ability.ts
  27. 0 311
      packages/plugin-abilities/tests/context-passing.test.ts
  28. 0 272
      packages/plugin-abilities/tests/enforcement.test.ts
  29. 0 471
      packages/plugin-abilities/tests/executor.test.ts
  30. 0 300
      packages/plugin-abilities/tests/integration.test.ts
  31. 0 131
      packages/plugin-abilities/tests/sdk.test.ts
  32. 0 131
      packages/plugin-abilities/tests/trigger.test.ts
  33. 0 174
      packages/plugin-abilities/tests/validator.test.ts
  34. 0 23
      packages/plugin-abilities/tsconfig.json
  35. 0 188
      pnpm-lock.yaml
  36. 1 6
      scripts/validation/detect-pr-changes.test.ts

+ 1 - 57
.github/workflows/packages-checks.yml

@@ -124,59 +124,10 @@ jobs:
       - name: Test (vitest)
         run: pnpm --dir packages/compatibility-layer test
 
-  # Non-blocking: packages/plugin-abilities is known-red today (92 tsc errors,
-  # 23 failing bun tests; its lint script is broken — eslint is not a dependency
-  # and no config exists). This job reports build/test status without failing
-  # PRs until the package is repaired. Once it is green, promote the build and
-  # test commands to normal failing steps and gate the summary on this job.
-  plugin-abilities-checks:
-    name: Plugin Abilities (build + test, non-blocking)
-    runs-on: ubuntu-latest
-    timeout-minutes: 10
-    needs: check-changes
-    if: needs.check-changes.outputs.has-packages == 'true'
-    outputs:
-      status: ${{ steps.nonblocking.outputs.status }}
-
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
-        with:
-          persist-credentials: false
-
-      - name: Setup Bun
-        uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
-        with:
-          bun-version: 1.3.14
-
-      - name: Setup pnpm
-        uses: pnpm/action-setup@b0f76dfb45f55f8421693e4803ac7bb65143bd34 # v6
-
-      - name: Setup Node.js
-        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
-        with:
-          node-version: '20'
-          cache: 'pnpm'
-          cache-dependency-path: 'pnpm-lock.yaml'
-
-      - name: Install workspace dependencies
-        run: pnpm install --frozen-lockfile
-
-      - name: Build and test (non-blocking)
-        id: nonblocking
-        run: |
-          STATUS=pass
-          pnpm --dir packages/plugin-abilities run build || STATUS=fail
-          pnpm --dir packages/plugin-abilities test || STATUS=fail
-          echo "status=$STATUS" >> "$GITHUB_OUTPUT"
-          if [ "$STATUS" = "fail" ]; then
-            echo "::warning title=plugin-abilities red::packages/plugin-abilities build/test failing (pre-existing, non-blocking)"
-          fi
-
   summary:
     name: Packages Checks Summary
     runs-on: ubuntu-latest
-    needs: [check-changes, cli-checks, compatibility-layer-checks, plugin-abilities-checks]
+    needs: [check-changes, cli-checks, compatibility-layer-checks]
     if: always()
 
     steps:
@@ -198,13 +149,6 @@ jobs:
           report "CLI" "${{ needs.cli-checks.result }}"
           report "Compatibility Layer" "${{ needs.compatibility-layer-checks.result }}"
 
-          case "${{ needs.plugin-abilities-checks.result }}/${{ needs.plugin-abilities-checks.outputs.status }}" in
-            success/pass) echo "✅ **Plugin Abilities (non-blocking):** Passed" >> $GITHUB_STEP_SUMMARY ;;
-            success/fail) echo "⚠️ **Plugin Abilities (non-blocking):** Failing (pre-existing, does not block this PR)" >> $GITHUB_STEP_SUMMARY ;;
-            skipped/*) echo "⏭️ **Plugin Abilities (non-blocking):** Skipped (no packages/** changes)" >> $GITHUB_STEP_SUMMARY ;;
-            *) echo "⚠️ **Plugin Abilities (non-blocking):** ${{ needs.plugin-abilities-checks.result }}" >> $GITHUB_STEP_SUMMARY ;;
-          esac
-
           echo "" >> $GITHUB_STEP_SUMMARY
 
           FAILED=0

+ 0 - 855
packages/plugin-abilities/ARCHITECTURE.md

@@ -1,855 +0,0 @@
-# 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.

+ 0 - 180
packages/plugin-abilities/MINIMAL_TEST.md

@@ -1,180 +0,0 @@
-# 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.

+ 0 - 320
packages/plugin-abilities/QUICK_REFERENCE.md

@@ -1,320 +0,0 @@
-# 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.

+ 0 - 401
packages/plugin-abilities/README.md

@@ -1,401 +0,0 @@
-# @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 Control 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

+ 0 - 187
packages/plugin-abilities/SIMPLIFICATION_SUMMARY.md

@@ -1,187 +0,0 @@
-# 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.

+ 0 - 177
packages/plugin-abilities/docs/GETTING_STARTED.md

@@ -1,177 +0,0 @@
-# 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

+ 0 - 72
packages/plugin-abilities/examples/deploy/ability.yaml

@@ -1,72 +0,0 @@
-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

+ 0 - 44
packages/plugin-abilities/examples/test-suite/ability.yaml

@@ -1,44 +0,0 @@
-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

+ 0 - 32
packages/plugin-abilities/examples/test/ability.yaml

@@ -1,32 +0,0 @@
-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

+ 0 - 61
packages/plugin-abilities/package.json

@@ -1,61 +0,0 @@
-{
-  "name": "@openagents/plugin-abilities",
-  "version": "1.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 Control",
-  "license": "MIT",
-  "dependencies": {
-    "@opencode-ai/plugin": "1.0.223",
-    "glob": "10.5.0",
-    "yaml": "2.8.2",
-    "zod": "3.25.76"
-  },
-  "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"
-  }
-}

+ 0 - 72
packages/plugin-abilities/src/context/discovery.ts

@@ -1,72 +0,0 @@
-import { glob } from 'glob';
-import path from 'path';
-import fs from 'fs/promises';
-import { parse as parseYaml } from 'yaml';
-import { ContextDefinitionSchema, type ContextDefinition, type LoadedContext } from './types.js';
-
-export interface DiscoveryOptions {
-  rootDir?: string;
-  contextDir?: string;
-}
-
-export class ContextDiscovery {
-  private rootDir: string;
-  private contextDir: string;
-
-  constructor(options: DiscoveryOptions = {}) {
-    this.rootDir = options.rootDir || process.cwd();
-    this.contextDir = options.contextDir || path.join(this.rootDir, '.opencode', 'context');
-  }
-
-  async discover(): Promise<ContextDefinition[]> {
-    const files = await glob('**/*.{yaml,yml,json}', {
-      cwd: this.contextDir,
-      ignore: ['node_modules/**'],
-    });
-
-    const definitions: ContextDefinition[] = [];
-
-    for (const file of files) {
-      const filePath = path.join(this.contextDir, file);
-      try {
-        const content = await fs.readFile(filePath, 'utf-8');
-        const parsed = file.endsWith('.json') ? JSON.parse(content) : parseYaml(content);
-        
-        // Handle array of definitions or single definition
-        const items = Array.isArray(parsed) ? parsed : [parsed];
-        
-        for (const item of items) {
-          const result = ContextDefinitionSchema.safeParse(item);
-          if (result.success) {
-            definitions.push(result.data);
-          } else {
-            console.warn(`Invalid context definition in ${file}:`, result.error.format());
-          }
-        }
-      } catch (error) {
-        console.warn(`Failed to load context file ${file}:`, error);
-      }
-    }
-
-    return definitions;
-  }
-
-  async loadContext(definition: ContextDefinition): Promise<LoadedContext | null> {
-    if (definition.type === 'file') {
-      const filePath = path.resolve(this.rootDir, definition.path);
-      try {
-        const content = await fs.readFile(filePath, 'utf-8');
-        return {
-          definition,
-          content,
-          source: filePath,
-        };
-      } catch (error) {
-        console.warn(`Failed to read context file ${definition.path}:`, error);
-        return null;
-      }
-    }
-    // TODO: Handle URL and API types
-    return null;
-  }
-}

+ 0 - 35
packages/plugin-abilities/src/context/types.ts

@@ -1,35 +0,0 @@
-import { z } from 'zod';
-
-export const ContextTypeSchema = z.enum(['file', 'url', 'api', 'snippet']);
-
-export const ContextDefinitionSchema = z.object({
-  id: z.string(),
-  type: ContextTypeSchema,
-  path: z.string(),
-  description: z.string().optional(),
-  priority: z.number().optional(),
-  metadata: z.record(z.unknown()).optional(),
-});
-
-export type ContextDefinition = z.infer<typeof ContextDefinitionSchema>;
-
-export interface LoadedContext {
-  definition: ContextDefinition;
-  content: string;
-  source: string;
-}
-
-export const SkillPermissionSchema = z.object({
-  skill: z.string(),
-  tools: z.array(z.string()).optional(),
-  resources: z.array(z.string()).optional(),
-  description: z.string().optional(),
-});
-
-export const AgentPermissionsSchema = z.object({
-  agent: z.string(),
-  permissions: z.array(SkillPermissionSchema),
-});
-
-export type SkillPermission = z.infer<typeof SkillPermissionSchema>;
-export type AgentPermissions = z.infer<typeof AgentPermissionsSchema>;

+ 0 - 59
packages/plugin-abilities/src/executor/execution-manager.ts

@@ -1,59 +0,0 @@
-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
-  }
-}

+ 0 - 241
packages/plugin-abilities/src/executor/index.ts

@@ -1,241 +0,0 @@
-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')
-}

+ 0 - 39
packages/plugin-abilities/src/index.ts

@@ -1,39 +0,0 @@
-/**
- * @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'
-export { PermissionValidator } from './validator/permissions.js'
-
-// Context Discovery
-export { ContextDiscovery } from './context/discovery.js'
-export type { ContextDefinition, LoadedContext, AgentPermissions } from './context/types.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'

+ 0 - 164
packages/plugin-abilities/src/loader/index.ts

@@ -1,164 +0,0 @@
-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,
-  }))
-}

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

@@ -1,227 +0,0 @@
-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

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

@@ -1,801 +0,0 @@
-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

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

@@ -1,259 +0,0 @@
-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 }

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

@@ -1,124 +0,0 @@
-/**
- * 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>
-}

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

@@ -1,356 +0,0 @@
-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 }

+ 0 - 45
packages/plugin-abilities/src/validator/permissions.ts

@@ -1,45 +0,0 @@
-import { AgentPermissionsSchema, type AgentPermissions, type SkillPermission } from '../context/types.js';
-
-export interface PermissionValidationResult {
-  valid: boolean;
-  errors: string[];
-}
-
-export class PermissionValidator {
-  validateAgentPermissions(data: unknown): PermissionValidationResult {
-    const result = AgentPermissionsSchema.safeParse(data);
-    
-    if (!result.success) {
-      return {
-        valid: false,
-        errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`),
-      };
-    }
-
-    return {
-      valid: true,
-      errors: [],
-    };
-  }
-
-  checkSkillAccess(agentPermissions: AgentPermissions, skillName: string, toolName: string): boolean {
-    const permission = agentPermissions.permissions.find(p => p.skill === skillName);
-    
-    if (!permission) {
-      // Default deny if no explicit permission for skill
-      return false;
-    }
-
-    if (!permission.tools) {
-      // If tools not specified, assume strict/deny or allow all?
-      // Security best practice: Default deny.
-      return false;
-    }
-
-    if (permission.tools.includes('*') || permission.tools.includes(toolName)) {
-      return true;
-    }
-
-    return false;
-  }
-}

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

@@ -1,82 +0,0 @@
-#!/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)
-})

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

@@ -1,87 +0,0 @@
-#!/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 Control' }
-  })
-  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)

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

@@ -1,46 +0,0 @@
-#!/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)

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

@@ -1,311 +0,0 @@
-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')
-  })
-})

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

@@ -1,272 +0,0 @@
-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]')
-    })
-  })
-})

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

@@ -1,471 +0,0 @@
-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')
-  })
-})

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

@@ -1,300 +0,0 @@
-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')
-  })
-})

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

@@ -1,131 +0,0 @@
-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()
-  })
-})

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

@@ -1,131 +0,0 @@
-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)
-    })
-  })
-})

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

@@ -1,174 +0,0 @@
-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)
-  })
-})

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

@@ -1,23 +0,0 @@
-{
-  "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"]
-}

+ 0 - 188
pnpm-lock.yaml

@@ -132,31 +132,6 @@ importers:
         specifier: ^1.6.0
         version: 1.6.1(@types/node@20.19.43)
 
-  packages/plugin-abilities:
-    dependencies:
-      '@opencode-ai/plugin':
-        specifier: 1.0.223
-        version: 1.0.223
-      glob:
-        specifier: 10.5.0
-        version: 10.5.0
-      yaml:
-        specifier: 2.8.2
-        version: 2.8.2
-      zod:
-        specifier: 3.25.76
-        version: 3.25.76
-    devDependencies:
-      '@types/bun':
-        specifier: ^1.0.0
-        version: 1.3.14
-      '@types/node':
-        specifier: ^20.10.0
-        version: 20.19.43
-      typescript:
-        specifier: ^5.3.0
-        version: 5.9.3
-
 packages:
 
   '@ampproject/remapping@2.3.0':
@@ -508,10 +483,6 @@ packages:
     resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
     deprecated: Use @eslint/object-schema instead
 
-  '@isaacs/cliui@8.0.2':
-    resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
-    engines: {node: '>=12'}
-
   '@istanbuljs/schema@0.1.6':
     resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
     engines: {node: '>=8'}
@@ -545,19 +516,9 @@ packages:
     resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
     engines: {node: '>= 8'}
 
-  '@opencode-ai/plugin@1.0.223':
-    resolution: {integrity: sha512-ZQAB7woEWHTpDlZrr+WYwIFI/QrmPblGk1nYLRObtpdMFoP8e2zLwE61j0IL4eBrgWY23+Xc2MrALZnkWL4O2Q==}
-
-  '@opencode-ai/sdk@1.0.223':
-    resolution: {integrity: sha512-oKJ6QjsviE+lt6cpGu0lL2kWuoj84ZkWvwieyqHEQ2pJunAJqUzhmIhzep0QyDax1/+UXhBWfrnciNt48ch66w==}
-
   '@opencode-ai/sdk@1.0.90':
     resolution: {integrity: sha512-uixOHqVR9X6S8slA3cWM3DpTCbEmNMUk2KyB5/6veqe/Mr/+Un1lj5sYU1/DNoV+xPpCEuUImbQvpTfzP3TZBA==}
 
-  '@pkgjs/parseargs@0.11.0':
-    resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
-    engines: {node: '>=14'}
-
   '@rollup/rollup-android-arm-eabi@4.62.2':
     resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==}
     cpu: [arm]
@@ -901,10 +862,6 @@ packages:
     resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
     engines: {node: '>=10'}
 
-  ansi-styles@6.2.3:
-    resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
-    engines: {node: '>=12'}
-
   argparse@1.0.10:
     resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
 
@@ -1022,18 +979,9 @@ packages:
     resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
     engines: {node: '>=6.0.0'}
 
-  eastasianwidth@0.2.0:
-    resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
-
   emoji-regex@10.6.0:
     resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
 
-  emoji-regex@8.0.0:
-    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
-
-  emoji-regex@9.2.2:
-    resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
-
   esbuild@0.21.5:
     resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
     engines: {node: '>=12'}
@@ -1143,10 +1091,6 @@ packages:
   flatted@3.4.2:
     resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
 
-  foreground-child@3.3.1:
-    resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
-    engines: {node: '>=14'}
-
   fs.realpath@1.0.0:
     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
 
@@ -1174,11 +1118,6 @@ packages:
     resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
     engines: {node: '>=10.13.0'}
 
-  glob@10.5.0:
-    resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
-    deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
-    hasBin: true
-
   glob@13.0.6:
     resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
     engines: {node: 18 || 20 || >=22}
@@ -1240,10 +1179,6 @@ packages:
     resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
     engines: {node: '>=0.10.0'}
 
-  is-fullwidth-code-point@3.0.0:
-    resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
-    engines: {node: '>=8'}
-
   is-glob@4.0.3:
     resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
     engines: {node: '>=0.10.0'}
@@ -1291,9 +1226,6 @@ packages:
     resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
     engines: {node: '>=8'}
 
-  jackspeak@3.4.3:
-    resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
-
   js-tokens@9.0.1:
     resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
 
@@ -1358,9 +1290,6 @@ packages:
   loupe@2.3.7:
     resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
 
-  lru-cache@10.4.3:
-    resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
-
   lru-cache@11.5.2:
     resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
     engines: {node: 20 || >=22}
@@ -1465,9 +1394,6 @@ packages:
     resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
     engines: {node: '>=10'}
 
-  package-json-from-dist@1.0.1:
-    resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
-
   parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
@@ -1488,10 +1414,6 @@ packages:
     resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
     engines: {node: '>=12'}
 
-  path-scurry@1.11.1:
-    resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
-    engines: {node: '>=16 || 14 >=14.18'}
-
   path-scurry@2.0.2:
     resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
     engines: {node: 18 || 20 || >=22}
@@ -1620,14 +1542,6 @@ packages:
     resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
     engines: {node: '>=18'}
 
-  string-width@4.2.3:
-    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
-    engines: {node: '>=8'}
-
-  string-width@5.1.2:
-    resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
-    engines: {node: '>=12'}
-
   string-width@7.2.0:
     resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
     engines: {node: '>=18'}
@@ -1793,14 +1707,6 @@ packages:
     resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
     engines: {node: '>=0.10.0'}
 
-  wrap-ansi@7.0.0:
-    resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
-    engines: {node: '>=10'}
-
-  wrap-ansi@8.1.0:
-    resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
-    engines: {node: '>=12'}
-
   wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 
@@ -1809,11 +1715,6 @@ packages:
     engines: {node: '>= 14.6'}
     hasBin: true
 
-  yaml@2.8.2:
-    resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
-    engines: {node: '>= 14.6'}
-    hasBin: true
-
   yocto-queue@0.1.0:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
@@ -1825,9 +1726,6 @@ packages:
   zod@3.25.76:
     resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
 
-  zod@4.1.8:
-    resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==}
-
 snapshots:
 
   '@ampproject/remapping@2.3.0':
@@ -2032,15 +1930,6 @@ snapshots:
 
   '@humanwhocodes/object-schema@2.0.3': {}
 
-  '@isaacs/cliui@8.0.2':
-    dependencies:
-      string-width: 5.1.2
-      string-width-cjs: string-width@4.2.3
-      strip-ansi: 7.2.0
-      strip-ansi-cjs: strip-ansi@6.0.1
-      wrap-ansi: 8.1.0
-      wrap-ansi-cjs: wrap-ansi@7.0.0
-
   '@istanbuljs/schema@0.1.6': {}
 
   '@jest/schemas@29.6.3':
@@ -2073,18 +1962,8 @@ snapshots:
       '@nodelib/fs.scandir': 2.1.5
       fastq: 1.20.1
 
-  '@opencode-ai/plugin@1.0.223':
-    dependencies:
-      '@opencode-ai/sdk': 1.0.223
-      zod: 4.1.8
-
-  '@opencode-ai/sdk@1.0.223': {}
-
   '@opencode-ai/sdk@1.0.90': {}
 
-  '@pkgjs/parseargs@0.11.0':
-    optional: true
-
   '@rollup/rollup-android-arm-eabi@4.62.2':
     optional: true
 
@@ -2439,8 +2318,6 @@ snapshots:
 
   ansi-styles@5.2.0: {}
 
-  ansi-styles@6.2.3: {}
-
   argparse@1.0.10:
     dependencies:
       sprintf-js: 1.0.3
@@ -2545,14 +2422,8 @@ snapshots:
     dependencies:
       esutils: 2.0.3
 
-  eastasianwidth@0.2.0: {}
-
   emoji-regex@10.6.0: {}
 
-  emoji-regex@8.0.0: {}
-
-  emoji-regex@9.2.2: {}
-
   esbuild@0.21.5:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.21.5
@@ -2747,11 +2618,6 @@ snapshots:
 
   flatted@3.4.2: {}
 
-  foreground-child@3.3.1:
-    dependencies:
-      cross-spawn: 7.0.6
-      signal-exit: 4.1.0
-
   fs.realpath@1.0.0: {}
 
   fsevents@2.3.3:
@@ -2771,15 +2637,6 @@ snapshots:
     dependencies:
       is-glob: 4.0.3
 
-  glob@10.5.0:
-    dependencies:
-      foreground-child: 3.3.1
-      jackspeak: 3.4.3
-      minimatch: 9.0.9
-      minipass: 7.1.3
-      package-json-from-dist: 1.0.1
-      path-scurry: 1.11.1
-
   glob@13.0.6:
     dependencies:
       minimatch: 10.2.5
@@ -2843,8 +2700,6 @@ snapshots:
 
   is-extglob@2.1.1: {}
 
-  is-fullwidth-code-point@3.0.0: {}
-
   is-glob@4.0.3:
     dependencies:
       is-extglob: 2.1.1
@@ -2884,12 +2739,6 @@ snapshots:
       html-escaper: 2.0.2
       istanbul-lib-report: 3.0.1
 
-  jackspeak@3.4.3:
-    dependencies:
-      '@isaacs/cliui': 8.0.2
-    optionalDependencies:
-      '@pkgjs/parseargs': 0.11.0
-
   js-tokens@9.0.1: {}
 
   js-yaml@3.15.0:
@@ -2950,8 +2799,6 @@ snapshots:
     dependencies:
       get-func-name: 2.0.2
 
-  lru-cache@10.4.3: {}
-
   lru-cache@11.5.2: {}
 
   magic-string@0.30.21:
@@ -3063,8 +2910,6 @@ snapshots:
     dependencies:
       p-limit: 3.1.0
 
-  package-json-from-dist@1.0.1: {}
-
   parent-module@1.0.1:
     dependencies:
       callsites: 3.1.0
@@ -3077,11 +2922,6 @@ snapshots:
 
   path-key@4.0.0: {}
 
-  path-scurry@1.11.1:
-    dependencies:
-      lru-cache: 10.4.3
-      minipass: 7.1.3
-
   path-scurry@2.0.2:
     dependencies:
       lru-cache: 11.5.2
@@ -3206,18 +3046,6 @@ snapshots:
 
   stdin-discarder@0.2.2: {}
 
-  string-width@4.2.3:
-    dependencies:
-      emoji-regex: 8.0.0
-      is-fullwidth-code-point: 3.0.0
-      strip-ansi: 6.0.1
-
-  string-width@5.1.2:
-    dependencies:
-      eastasianwidth: 0.2.0
-      emoji-regex: 9.2.2
-      strip-ansi: 7.2.0
-
   string-width@7.2.0:
     dependencies:
       emoji-regex: 10.6.0
@@ -3364,28 +3192,12 @@ snapshots:
 
   word-wrap@1.2.5: {}
 
-  wrap-ansi@7.0.0:
-    dependencies:
-      ansi-styles: 4.3.0
-      string-width: 4.2.3
-      strip-ansi: 6.0.1
-
-  wrap-ansi@8.1.0:
-    dependencies:
-      ansi-styles: 6.2.3
-      string-width: 5.1.2
-      strip-ansi: 7.2.0
-
   wrappy@1.0.2: {}
 
   yaml@2.8.1: {}
 
-  yaml@2.8.2: {}
-
   yocto-queue@0.1.0: {}
 
   yocto-queue@1.2.2: {}
 
   zod@3.25.76: {}
-
-  zod@4.1.8: {}

+ 1 - 6
scripts/validation/detect-pr-changes.test.ts

@@ -87,10 +87,6 @@ describe('classifyChangedPaths', () => {
       ...NO_CHANGES,
       'has-packages': true,
     })
-    expect(classifyChangedPaths(['packages/plugin-abilities/src/index.ts'])).toEqual({
-      ...NO_CHANGES,
-      'has-packages': true,
-    })
   })
 
   test('detects shared workspace dependency changes', () => {
@@ -298,10 +294,9 @@ describe('Packages checks workflow contract', () => {
 
     expect(workflow).toContain('cli-checks:')
     expect(workflow).toContain('compatibility-layer-checks:')
-    expect(workflow).toContain('plugin-abilities-checks:')
 
     const gates = workflow.match(/needs\.check-changes\.outputs\.has-packages == 'true'/g) ?? []
-    expect(gates.length).toBeGreaterThanOrEqual(3)
+    expect(gates.length).toBeGreaterThanOrEqual(2)
   })
 
   test('triggers on packages/** pull request changes', async () => {