|
@@ -0,0 +1,855 @@
|
|
|
|
|
+# Abilities Plugin: Architecture & System Overview
|
|
|
|
|
+
|
|
|
|
|
+## Executive Summary
|
|
|
|
|
+
|
|
|
|
|
+The **Abilities Plugin** is an OpenCode plugin that enforces deterministic, step-by-step workflow execution for AI agents. It solves the core problem with traditional skills: **LLMs ignore them**. By using enforcement hooks and structured workflows, Abilities guarantee that agents follow prescribed steps in order, without deviation.
|
|
|
|
|
+
|
|
|
|
|
+### Core Problem It Solves
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+Traditional Skills Abilities
|
|
|
|
|
+───────────────────────────────── ──────────────────────────
|
|
|
|
|
+Agent sees skill definition → Ability enforces execution
|
|
|
|
|
+Agent can ignore it → Agent MUST follow steps
|
|
|
|
|
+Execution is non-deterministic → Execution is deterministic
|
|
|
|
|
+No validation between steps → Each step validated
|
|
|
|
|
+Multi-agent coordination fails → Agent-specific abilities work
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## System Architecture Overview
|
|
|
|
|
+
|
|
|
|
|
+### High-Level Flow
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+User Request
|
|
|
|
|
+ ↓
|
|
|
|
|
+┌─────────────────────────────────┐
|
|
|
|
|
+│ OpenCode Chat Message Received │
|
|
|
|
|
+ ↓
|
|
|
|
|
+┌──────────────────────────────────────┐
|
|
|
|
|
+│ AbilitiesPlugin Hooks (opencode-plugin.ts)
|
|
|
|
|
+│ ├─ chat.message: Detect ability trigger
|
|
|
|
|
+│ ├─ tool.execute.before: Block unauthorized tools
|
|
|
|
|
+│ └─ event: Manage execution state
|
|
|
|
|
+ ↓
|
|
|
|
|
+┌──────────────────────────────────────┐
|
|
|
|
|
+│ If ability triggered:
|
|
|
|
|
+│ ├─ Load ability definition (loader/)
|
|
|
|
|
+│ ├─ Validate inputs (validator/)
|
|
|
|
|
+│ └─ Execute steps (executor/)
|
|
|
|
|
+ ↓
|
|
|
|
|
+┌──────────────────────────────────────┐
|
|
|
|
|
+│ Step Execution (ExecutionManager)
|
|
|
|
|
+│ ├─ Sequential execution with dependencies
|
|
|
|
|
+│ ├─ Output context passing
|
|
|
|
|
+│ ├─ Step-level validation
|
|
|
|
|
+│ └─ Error handling & recovery
|
|
|
|
|
+ ↓
|
|
|
|
|
+┌──────────────────────────────────────┐
|
|
|
|
|
+│ Enforcement Applied
|
|
|
|
|
+│ ├─ Tool blocking (only allowed tools)
|
|
|
|
|
+│ ├─ Context injection (ability status)
|
|
|
|
|
+│ └─ Step-by-step control
|
|
|
|
|
+ ↓
|
|
|
|
|
+Agent sees ability context and executes
|
|
|
|
|
+as instructed (not as LLM desires)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Module Architecture
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+src/
|
|
|
|
|
+├── opencode-plugin.ts [ENTRY POINT] Main plugin implementation
|
|
|
|
|
+│ ├─ Hooks: event, chat.message, tool.execute.before
|
|
|
|
|
+│ ├─ Tools: ability.list, ability.run, ability.status, ability.cancel
|
|
|
|
|
+│ └─ Enforcement logic & context injection
|
|
|
|
|
+│
|
|
|
|
|
+├── loader/
|
|
|
|
|
+│ └─ index.ts [DISCOVERY] Load ability YAML files
|
|
|
|
|
+│ ├─ loadAbilities(): Discover & parse all abilities
|
|
|
|
|
+│ ├─ loadAbility(): Get specific ability
|
|
|
|
|
+│ └─ listAbilities(): Format for display
|
|
|
|
|
+│
|
|
|
|
|
+├── validator/
|
|
|
|
|
+│ └─ index.ts [VALIDATION] Ensure abilities are valid
|
|
|
|
|
+│ ├─ validateAbility(): Check structure, dependencies, step types
|
|
|
|
|
+│ ├─ validateInputs(): Type-check user inputs against schema
|
|
|
|
|
+│ └─ validateSteps(): Ensure no circular dependencies
|
|
|
|
|
+│
|
|
|
|
|
+├── executor/
|
|
|
|
|
+│ ├─ index.ts [EXECUTION] Run ability steps
|
|
|
|
|
+│ │ ├─ executeAbility(): Main orchestrator
|
|
|
|
|
+│ │ ├─ executeScriptStep(): Run shell commands
|
|
|
|
|
+│ │ ├─ executeAgentStep(): Call agents
|
|
|
|
|
+│ │ ├─ executeSkillStep(): Load skills
|
|
|
|
|
+│ │ ├─ executeApprovalStep(): Request approval
|
|
|
|
|
+│ │ └─ executeWorkflowStep(): Run nested abilities
|
|
|
|
|
+│ │
|
|
|
|
|
+│ └─ execution-manager.ts [STATE] Track active executions
|
|
|
|
|
+│ ├─ ExecutionManager: Lifecycle management
|
|
|
|
|
+│ ├─ execute(): Start new execution
|
|
|
|
|
+│ ├─ getActive(): Current execution status
|
|
|
|
|
+│ ├─ cancelActive(): Stop active ability
|
|
|
|
|
+│ └─ cleanup(): GC & resource management
|
|
|
|
|
+│
|
|
|
|
|
+├── types/
|
|
|
|
|
+│ └─ index.ts [TYPES] TypeScript definitions
|
|
|
|
|
+│ ├─ Ability, Step types
|
|
|
|
|
+│ ├─ Execution state types
|
|
|
|
|
+│ └─ Input/output schemas
|
|
|
|
|
+│
|
|
|
|
|
+└── index.ts [EXPORTS] Public API
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Module Responsibilities
|
|
|
|
|
+
|
|
|
|
|
+### 1. **opencode-plugin.ts** - Main Plugin & Enforcement
|
|
|
|
|
+**Responsibility**: Interface between OpenCode and the abilities system
|
|
|
|
|
+
|
|
|
|
|
+**Key Functions**:
|
|
|
|
|
+- `AbilitiesPlugin` - Main async factory function that returns hooks
|
|
|
|
|
+- `matchesTrigger()` - Detect if user text matches ability keywords/patterns
|
|
|
|
|
+- `detectAbility()` - Find matching ability from user message
|
|
|
|
|
+- `showToast()` - Display UI notifications
|
|
|
|
|
+- `createExecutorContext()` - Build execution environment
|
|
|
|
|
+- `buildAbilityContextInjection()` - Format ability status for agent
|
|
|
|
|
+
|
|
|
|
|
+**Hooks Implemented**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+{
|
|
|
|
|
+ event() // Handle session lifecycle (create/delete)
|
|
|
|
|
+ config() // Load plugin configuration
|
|
|
|
|
+ 'chat.message'() // Intercept messages, detect abilities, inject context
|
|
|
|
|
+ 'tool.execute.before()' // Block unauthorized tools during steps
|
|
|
|
|
+ tool: { // Register custom tools
|
|
|
|
|
+ 'ability.list',
|
|
|
|
|
+ 'ability.run',
|
|
|
|
|
+ 'ability.status',
|
|
|
|
|
+ 'ability.cancel'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Enforcement Strategy**:
|
|
|
|
|
+- **Message Interception**: When user types, check if it matches ability triggers
|
|
|
|
|
+- **Tool Blocking**: During ability execution, only allow tools for current step type
|
|
|
|
|
+- **Context Injection**: Add ability progress/instructions to every message
|
|
|
|
|
+- **State Tracking**: ExecutionManager tracks active executions per session
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 2. **loader/index.ts** - Ability Discovery
|
|
|
|
|
+**Responsibility**: Find and parse YAML ability definitions from filesystem
|
|
|
|
|
+
|
|
|
|
|
+**Key Functions**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+loadAbilities(options) // Discover all abilities in directories
|
|
|
|
|
+ └─ discoverAbilities() // Glob for *.yaml files
|
|
|
|
|
+ └─ loadAbilityFile() // Parse YAML → Ability object
|
|
|
|
|
+
|
|
|
|
|
+loadAbility(name) // Get specific ability by name
|
|
|
|
|
+
|
|
|
|
|
+listAbilities(map) // Format abilities for display
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Globbing Strategy** (Limited scope):
|
|
|
|
|
+```typescript
|
|
|
|
|
+const ABILITY_PATTERNS = [
|
|
|
|
|
+ '*.yaml', // Single-level files
|
|
|
|
|
+ '*/ability.yaml', // Dir with ability.yaml
|
|
|
|
|
+ '*/*.yaml', // Dir with YAML files
|
|
|
|
|
+ '*/*/ability.yaml' // Two-level nesting (max)
|
|
|
|
|
+]
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Why Limited Patterns?**
|
|
|
|
|
+- Prevents scanning entire project (performance)
|
|
|
|
|
+- Stops accidental loading of unrelated YAML files
|
|
|
|
|
+- Encourages organized directory structure
|
|
|
|
|
+
|
|
|
|
|
+**Output**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+Map<string, LoadedAbility> {
|
|
|
|
|
+ 'deploy': { ability, filePath, source }
|
|
|
|
|
+ 'deploy/staging': { ability, filePath, source }
|
|
|
|
|
+ 'test-suite': { ability, filePath, source }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 3. **validator/index.ts** - Structure & Input Validation
|
|
|
|
|
+**Responsibility**: Ensure abilities are well-formed and inputs are valid
|
|
|
|
|
+
|
|
|
|
|
+**Key Functions**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+validateAbility(ability) // Check structure
|
|
|
|
|
+ └─ Validates:
|
|
|
|
|
+ ├─ name field exists
|
|
|
|
|
+ ├─ steps array non-empty
|
|
|
|
|
+ ├─ no duplicate step IDs
|
|
|
|
|
+ ├─ no circular dependencies
|
|
|
|
|
+ ├─ all dependencies exist
|
|
|
|
|
+ ├─ step types valid
|
|
|
|
|
+ └─ nested abilities exist
|
|
|
|
|
+
|
|
|
|
|
+validateInputs(ability, inputs) // Type-check user inputs
|
|
|
|
|
+ └─ For each input definition:
|
|
|
|
|
+ ├─ required field check
|
|
|
|
|
+ ├─ type validation (string/number/object)
|
|
|
|
|
+ ├─ pattern regex validation
|
|
|
|
|
+ ├─ enum value check
|
|
|
|
|
+ ├─ min/max range check
|
|
|
|
|
+ └─ default value handling
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Validation Output**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+{
|
|
|
|
|
+ valid: boolean
|
|
|
|
|
+ errors: Array<{
|
|
|
|
|
+ path: string // e.g., "inputs.version"
|
|
|
|
|
+ message: string // "Must match pattern: ^v\d+\.\d+\.\d+$"
|
|
|
|
|
+ }>
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 4. **executor/index.ts** - Step Execution Engine
|
|
|
|
|
+**Responsibility**: Execute ability steps sequentially with dependency management
|
|
|
|
|
+
|
|
|
|
|
+**Key Functions**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+executeAbility(ability, inputs, ctx, options)
|
|
|
|
|
+ └─ buildExecutionOrder(steps) // Resolve dependencies
|
|
|
|
|
+ └─ executeStep(step, execution, ctx)
|
|
|
|
|
+ ├─ executeScriptStep() // Run: sh -c "command"
|
|
|
|
|
+ ├─ executeAgentStep() // Call agent with context
|
|
|
|
|
+ ├─ executeSkillStep() // Load skill
|
|
|
|
|
+ ├─ executeApprovalStep() // Request user approval
|
|
|
|
|
+ └─ executeWorkflowStep() // Run nested ability
|
|
|
|
|
+
|
|
|
|
|
+formatExecutionResult(execution) // Pretty-print results
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Step Types & Their Allowed Tools**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+ALLOWED_TOOLS_BY_STEP_TYPE = {
|
|
|
|
|
+ script: [], // No tools (runs deterministically)
|
|
|
|
|
+ agent: ['task', 'background_task'], // Only agent-calling tools
|
|
|
|
|
+ skill: ['skill', 'slashcommand'], // Skill-related tools
|
|
|
|
|
+ approval: ['ability.status'], // Read-only status tools
|
|
|
|
|
+ workflow: ['ability.run', 'ability.status'] // Run nested ability
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Variable Interpolation**:
|
|
|
|
|
+```yaml
|
|
|
|
|
+steps:
|
|
|
|
|
+ - run: "deploy {{inputs.version}} to {{inputs.env}}"
|
|
|
|
|
+ - run: "echo {{steps.test.output}}" # From previous step output
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Dependency Resolution**:
|
|
|
|
|
+```yaml
|
|
|
|
|
+steps:
|
|
|
|
|
+ - id: test
|
|
|
|
|
+ type: script
|
|
|
|
|
+ run: npm test
|
|
|
|
|
+
|
|
|
|
|
+ - id: build
|
|
|
|
|
+ needs: [test] # Runs after test completes
|
|
|
|
|
+ run: npm run build
|
|
|
|
|
+
|
|
|
|
|
+ - id: deploy
|
|
|
|
|
+ needs: [build] # Runs after build completes
|
|
|
|
|
+ run: ./deploy.sh
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 5. **executor/execution-manager.ts** - Lifecycle Management
|
|
|
|
|
+**Responsibility**: Track active executions, manage state, handle cleanup
|
|
|
|
|
+
|
|
|
|
|
+**Key Responsibilities**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+class ExecutionManager {
|
|
|
|
|
+ // Lifecycle
|
|
|
|
|
+ execute(ability, inputs, ctx) // Start new execution
|
|
|
|
|
+ getActive() // Get current execution
|
|
|
|
|
+ cancelActive() // Stop active ability
|
|
|
|
|
+
|
|
|
|
|
+ // State Management
|
|
|
|
|
+ updateStep(executionId, result) // Mark step complete
|
|
|
|
|
+ cancel(executionId) // Cancel by ID
|
|
|
|
|
+ get(id) // Retrieve execution history
|
|
|
|
|
+ list() // All executions (for debugging)
|
|
|
|
|
+
|
|
|
|
|
+ // Resource Management
|
|
|
|
|
+ cleanup() // Clean up timers & state
|
|
|
|
|
+ cleanupOldExecutions() // GC old executions (30 min TTL)
|
|
|
|
|
+ trimToMaxSize() // Keep last 50 executions max
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Cleanup Strategy**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+const EXECUTION_TTL = 30 * 60 * 1000 // Delete after 30 minutes
|
|
|
|
|
+const CLEANUP_INTERVAL = 5 * 60 * 1000 // Check every 5 minutes
|
|
|
|
|
+const MAX_STORED_EXECUTIONS = 50 // Keep last 50
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Why This Matters**:
|
|
|
|
|
+- Lazy timer initialization (doesn't create timers until first execution)
|
|
|
|
|
+- Automatic GC prevents memory leaks from long-running sessions
|
|
|
|
|
+- State persists across messages in same session
|
|
|
|
|
+- Timer uses `unref()` so it doesn't prevent process exit
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 6. **types/index.ts** - Type Definitions
|
|
|
|
|
+**Responsibility**: Provide TypeScript types for all data structures
|
|
|
|
|
+
|
|
|
|
|
+**Key Types**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+// Ability Definition
|
|
|
|
|
+interface Ability {
|
|
|
|
|
+ name: string
|
|
|
|
|
+ description: string
|
|
|
|
|
+ triggers?: {
|
|
|
|
|
+ keywords?: string[]
|
|
|
|
|
+ patterns?: string[] // Regex patterns
|
|
|
|
|
+ }
|
|
|
|
|
+ inputs?: Record<string, InputDefinition>
|
|
|
|
|
+ steps: Step[]
|
|
|
|
|
+ settings?: {
|
|
|
|
|
+ enforcement?: 'strict' | 'normal' | 'loose'
|
|
|
|
|
+ on_failure?: 'stop' | 'retry' | 'continue'
|
|
|
|
|
+ }
|
|
|
|
|
+ exclusive_agent?: string // Only this agent can run
|
|
|
|
|
+ compatible_agents?: string[] // Whitelist of agents
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Step Types
|
|
|
|
|
+type Step =
|
|
|
|
|
+ | ScriptStep
|
|
|
|
|
+ | AgentStep
|
|
|
|
|
+ | SkillStep
|
|
|
|
|
+ | ApprovalStep
|
|
|
|
|
+ | WorkflowStep
|
|
|
|
|
+
|
|
|
|
|
+interface ScriptStep {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ type: 'script'
|
|
|
|
|
+ description?: string
|
|
|
|
|
+ run: string // Shell command
|
|
|
|
|
+ cwd?: string // Working directory
|
|
|
|
|
+ env?: Record<string, string> // Environment variables
|
|
|
|
|
+ timeout?: string // '5m', '30s'
|
|
|
|
|
+ validation?: {
|
|
|
|
|
+ exit_code?: number
|
|
|
|
|
+ stdout_contains?: string
|
|
|
|
|
+ stderr_contains?: string
|
|
|
|
|
+ }
|
|
|
|
|
+ on_failure?: 'stop' | 'retry' | 'continue'
|
|
|
|
|
+ max_retries?: number
|
|
|
|
|
+ when?: string // Conditional: "inputs.env == 'prod'"
|
|
|
|
|
+ needs?: string[] // Dependencies
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Execution State
|
|
|
|
|
+interface AbilityExecution {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ ability: Ability
|
|
|
|
|
+ inputs: InputValues
|
|
|
|
|
+ status: 'running' | 'completed' | 'failed' | 'cancelled'
|
|
|
|
|
+ currentStep: Step | null
|
|
|
|
|
+ currentStepIndex: number
|
|
|
|
|
+ completedSteps: StepResult[]
|
|
|
|
|
+ pendingSteps: Step[]
|
|
|
|
|
+ startedAt: number
|
|
|
|
|
+ completedAt?: number
|
|
|
|
|
+ error?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface StepResult {
|
|
|
|
|
+ stepId: string
|
|
|
|
|
+ status: 'completed' | 'failed' | 'skipped'
|
|
|
|
|
+ output?: string
|
|
|
|
|
+ error?: string
|
|
|
|
|
+ startedAt: number
|
|
|
|
|
+ completedAt: number
|
|
|
|
|
+ duration: number
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Data Flow Example: Deploy Workflow
|
|
|
|
|
+
|
|
|
|
|
+### 1. User Types "Deploy v1.2.3"
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+User Message: "Deploy v1.2.3"
|
|
|
|
|
+ ↓
|
|
|
|
|
+chat.message hook intercepts
|
|
|
|
|
+ ↓
|
|
|
|
|
+detectAbility("Deploy v1.2.3")
|
|
|
|
|
+ ├─ Check: "deploy" keyword in message? ✓
|
|
|
|
|
+ ├─ Found: ability { name: "deploy", ... }
|
|
|
|
|
+ ↓
|
|
|
|
|
+Show ability suggestion:
|
|
|
|
|
+"## Ability Detected: deploy\n\n Deploy application..."
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2. User Runs: `/ability.run deploy version=v1.2.3`
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+ability.run tool executes:
|
|
|
|
|
+ ├─ Load ability: "deploy"
|
|
|
|
|
+ ├─ Validate inputs:
|
|
|
|
|
+ │ └─ version matches pattern: ^v\d+\.\d+\.\d+$ ✓
|
|
|
|
|
+ ├─ executionManager.execute(ability, {version: "v1.2.3"})
|
|
|
|
|
+ │
|
|
|
|
|
+ └─ Start execution:
|
|
|
|
|
+ ExecutionManager creates AbilityExecution {
|
|
|
|
|
+ id: "exec_1704067200000_abc123"
|
|
|
|
|
+ status: "running"
|
|
|
|
|
+ currentStep: steps[0]
|
|
|
|
|
+ }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3. Step 1: Test (Script)
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+Step: "test" (script)
|
|
|
|
|
+ ├─ Command: "npm test"
|
|
|
|
|
+ ├─ Run in shell:
|
|
|
|
|
+ │ ├─ stdout: "✓ 124 tests passed"
|
|
|
|
|
+ │ ├─ exit code: 0
|
|
|
|
|
+ │ └─ validation: exit_code == 0 ✓
|
|
|
|
|
+ ├─ Record result:
|
|
|
|
|
+ │ └─ StepResult { stepId: "test", status: "completed", output: "..." }
|
|
|
|
|
+ ├─ Inject context in next message:
|
|
|
|
|
+ │ "## Active Ability: deploy\nProgress: 1/3 steps\nCurrent Step: build..."
|
|
|
|
|
+ └─ Continue
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 4. Step 2: Build (Script, depends on test)
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+Step: "build" (script)
|
|
|
|
|
+ ├─ Needs: ["test"] ✓ (completed)
|
|
|
|
|
+ ├─ Command: "npm run build"
|
|
|
|
|
+ ├─ Tool check (tool.execute.before):
|
|
|
|
|
+ │ └─ Block: bash, write, edit (not allowed in script steps)
|
|
|
|
|
+ ├─ Execute...
|
|
|
|
|
+ ├─ Result: success
|
|
|
|
|
+ └─ Continue
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 5. Step 3: Deploy (Script)
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+Step: "deploy" (script)
|
|
|
|
|
+ ├─ Needs: ["build"] ✓ (completed)
|
|
|
|
|
+ ├─ Interpolate variables:
|
|
|
|
|
+ │ └─ "Deploy {{inputs.version}}" → "Deploy v1.2.3"
|
|
|
|
|
+ ├─ Run: "./deploy.sh v1.2.3"
|
|
|
|
|
+ ├─ Result: success
|
|
|
|
|
+ └─ Mark ability complete
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 6. Execution Complete
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+Set status: "completed"
|
|
|
|
|
+Save results: { completedSteps: [...], duration: "42.3s" }
|
|
|
|
|
+Return: "✅ Ability 'deploy' completed successfully"
|
|
|
|
|
+Clear activeExecution for next ability
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Enforcement Mechanisms
|
|
|
|
|
+
|
|
|
|
|
+### 1. Tool Blocking (tool.execute.before hook)
|
|
|
|
|
+
|
|
|
|
|
+**Problem**: Agent might try to call `bash` during a `script` step (redundant & risky)
|
|
|
|
|
+
|
|
|
|
|
+**Solution**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+async 'tool.execute.before'(input, output) {
|
|
|
|
|
+ if (!activeExecution) return // Not running ability, allow all
|
|
|
|
|
+
|
|
|
|
|
+ const currentStep = activeExecution.currentStep
|
|
|
|
|
+ const allowedTools = ALLOWED_TOOLS_BY_STEP_TYPE[currentStep.type]
|
|
|
|
|
+
|
|
|
|
|
+ if (enforcement === 'strict' && !allowedTools.includes(input.tool)) {
|
|
|
|
|
+ throw new Error(`Tool '${input.tool}' blocked in ${currentStep.type} step`)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Effect**: Agent cannot deviate from prescribed tool usage for current step
|
|
|
|
|
+
|
|
|
|
|
+### 2. Context Injection (chat.message hook)
|
|
|
|
|
+
|
|
|
|
|
+**Problem**: Agent might forget which step it's on or what to do next
|
|
|
|
|
+
|
|
|
|
|
+**Solution**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+async 'chat.message'(input, output) {
|
|
|
|
|
+ if (activeExecution?.status === 'running') {
|
|
|
|
|
+ // Inject ability context at start of every message
|
|
|
|
|
+ output.parts.unshift({
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: `## Active Ability: ${ability.name}\nProgress: 2/3 steps\nCurrent Step: deploy\nAction: Run ./deploy.sh v1.2.3`
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Effect**: Agent always sees context reminder, reducing deviation
|
|
|
|
|
+
|
|
|
|
|
+### 3. Ability Detection (chat.message keyword matching)
|
|
|
|
|
+
|
|
|
|
|
+**Problem**: User doesn't know they can run an ability
|
|
|
|
|
+
|
|
|
|
|
+**Solution**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+const detected = detectAbility(userText) // Check triggers
|
|
|
|
|
+if (detected) {
|
|
|
|
|
+ output.parts.unshift({
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: `## Ability Detected: ${detected.name}\n\n${detected.description}...`
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Effect**: Auto-discovery makes abilities more discoverable
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Configuration
|
|
|
|
|
+
|
|
|
|
|
+### In `.opencode/opencode.json`:
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "plugin": [
|
|
|
|
|
+ "file://../packages/plugin-abilities/src/opencode-plugin.ts"
|
|
|
|
|
+ ]
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Optional Config:
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "abilities": {
|
|
|
|
|
+ "enabled": true,
|
|
|
|
|
+ "auto_trigger": true,
|
|
|
|
|
+ "enforcement": "strict",
|
|
|
|
|
+ "directories": [
|
|
|
|
|
+ ".opencode/abilities",
|
|
|
|
|
+ "~/.config/opencode/abilities"
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Ability File Structure
|
|
|
|
|
+
|
|
|
|
|
+### Basic Example
|
|
|
|
|
+
|
|
|
|
|
+```yaml
|
|
|
|
|
+# .opencode/abilities/deploy/ability.yaml
|
|
|
|
|
+name: deploy
|
|
|
|
|
+description: Deploy application with safety checks
|
|
|
|
|
+
|
|
|
|
|
+triggers:
|
|
|
|
|
+ keywords:
|
|
|
|
|
+ - deploy
|
|
|
|
|
+ - ship
|
|
|
|
|
+ patterns:
|
|
|
|
|
+ - 'deploy.*v\d+\.\d+\.\d+'
|
|
|
|
|
+
|
|
|
|
|
+inputs:
|
|
|
|
|
+ version:
|
|
|
|
|
+ type: string
|
|
|
|
|
+ required: true
|
|
|
|
|
+ pattern: '^v\d+\.\d+\.\d+$'
|
|
|
|
|
+ environment:
|
|
|
|
|
+ type: string
|
|
|
|
|
+ enum: [dev, staging, prod]
|
|
|
|
|
+ default: staging
|
|
|
|
|
+
|
|
|
|
|
+steps:
|
|
|
|
|
+ - id: test
|
|
|
|
|
+ type: script
|
|
|
|
|
+ run: npm test
|
|
|
|
|
+ validation:
|
|
|
|
|
+ exit_code: 0
|
|
|
|
|
+
|
|
|
|
|
+ - id: build
|
|
|
|
|
+ type: script
|
|
|
|
|
+ needs: [test]
|
|
|
|
|
+ run: npm run build
|
|
|
|
|
+ validation:
|
|
|
|
|
+ exit_code: 0
|
|
|
|
|
+
|
|
|
|
|
+ - id: approve
|
|
|
|
|
+ type: approval
|
|
|
|
|
+ needs: [build]
|
|
|
|
|
+ prompt: "Deploy {{inputs.version}} to {{inputs.environment}}?"
|
|
|
|
|
+
|
|
|
|
|
+ - id: deploy
|
|
|
|
|
+ type: script
|
|
|
|
|
+ needs: [approve]
|
|
|
|
|
+ run: ./deploy.sh {{inputs.version}} {{inputs.environment}}
|
|
|
|
|
+ validation:
|
|
|
|
|
+ exit_code: 0
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Tools Available to Agents
|
|
|
|
|
+
|
|
|
|
|
+### ability.list
|
|
|
|
|
+Lists all available abilities
|
|
|
|
|
+```
|
|
|
|
|
+ability.list
|
|
|
|
|
+→ "- deploy: Deploy application...\n- test: Run tests..."
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### ability.run
|
|
|
|
|
+Execute an ability
|
|
|
|
|
+```
|
|
|
|
|
+ability.run { name: "deploy", inputs: { version: "v1.2.3" } }
|
|
|
|
|
+→ { status: "completed", ability: "deploy", result: "..." }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### ability.status
|
|
|
|
|
+Check active ability execution
|
|
|
|
|
+```
|
|
|
|
|
+ability.status
|
|
|
|
|
+→ { status: "running", ability: "deploy", currentStep: "build", progress: "2/3" }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### ability.cancel
|
|
|
|
|
+Cancel active ability
|
|
|
|
|
+```
|
|
|
|
|
+ability.cancel
|
|
|
|
|
+→ { status: "cancelled", message: "Ability cancelled" }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Execution Flow Diagram
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+┌──────────────────────────────────────────────────────────┐
|
|
|
|
|
+│ User Message → chat.message hook │
|
|
|
|
|
+└────────────────────┬─────────────────────────────────────┘
|
|
|
|
|
+ │
|
|
|
|
|
+ ┌───────────┴────────────┐
|
|
|
|
|
+ ▼ ▼
|
|
|
|
|
+ No ability match Ability detected
|
|
|
|
|
+ │ │
|
|
|
|
|
+ │ ┌─────┴──────┐
|
|
|
|
|
+ │ ▼ ▼
|
|
|
|
|
+ │ Auto-detect Show suggestion
|
|
|
|
|
+ │ (cool 10s) to user
|
|
|
|
|
+ │
|
|
|
|
|
+ ├─────────────────────────────────────┐
|
|
|
|
|
+ │ │
|
|
|
|
|
+ Allow normal User runs /ability.run
|
|
|
|
|
+ OpenCode flow │
|
|
|
|
|
+ ┌──────────┴───────────┐
|
|
|
|
|
+ ▼ ▼
|
|
|
|
|
+ Load ability Validate inputs
|
|
|
|
|
+ │ │
|
|
|
|
|
+ └──────────┬───────────┘
|
|
|
|
|
+ ▼
|
|
|
|
|
+ ExecutionManager.execute()
|
|
|
|
|
+ │
|
|
|
|
|
+ ┌───────────────┴────────────────┐
|
|
|
|
|
+ │ Build execution order (deps) │
|
|
|
|
|
+ ├───────────────────────────────┤
|
|
|
|
|
+ │ FOR each step: │
|
|
|
|
|
+ │ ├─ Evaluate 'when' condition │
|
|
|
|
|
+ │ ├─ Execute step type: │
|
|
|
|
|
+ │ │ ├─ script → shell cmd │
|
|
|
|
|
+ │ │ ├─ agent → agent call │
|
|
|
|
|
+ │ │ ├─ skill → load skill │
|
|
|
|
|
+ │ │ ├─ approval → ask user │
|
|
|
|
|
+ │ │ └─ workflow → nested run │
|
|
|
|
|
+ │ ├─ Validate output │
|
|
|
|
|
+ │ ├─ Pass context to next step │
|
|
|
|
|
+ │ └─ Record result │
|
|
|
|
|
+ │ │
|
|
|
|
|
+ ├─ On failure: │
|
|
|
|
|
+ │ ├─ Stop (default) │
|
|
|
|
|
+ │ ├─ Retry (with max retries) │
|
|
|
|
|
+ │ └─ Continue (ignore error) │
|
|
|
|
|
+ │ │
|
|
|
|
|
+ └───────────────┬────────────────┘
|
|
|
|
|
+ ▼
|
|
|
|
|
+ Return execution results
|
|
|
|
|
+ │
|
|
|
|
|
+ ┌───────────────────┼───────────────────┐
|
|
|
|
|
+ ▼ ▼ ▼
|
|
|
|
|
+ Save to history Show toast result Clear active
|
|
|
|
|
+ (50 max, 30m TTL) (success/error) execution
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Performance & Resource Considerations
|
|
|
|
|
+
|
|
|
|
|
+### Lazy Initialization
|
|
|
|
|
+- ExecutionManager timer only starts on first ability execution
|
|
|
|
|
+- Timer uses `unref()` so it doesn't block process exit
|
|
|
|
|
+- Prevents unnecessary resource usage for inactive plugins
|
|
|
|
|
+
|
|
|
|
|
+### Memory Management
|
|
|
|
|
+- Keep only last 50 executions in memory
|
|
|
|
|
+- Automatically delete executions older than 30 minutes
|
|
|
|
|
+- No memory leaks from long-running sessions
|
|
|
|
|
+
|
|
|
|
|
+### Search Scope
|
|
|
|
|
+- Glob patterns limited to 2 levels deep (`*/*/ability.yaml`)
|
|
|
|
|
+- Prevents scanning entire project (could be thousands of files)
|
|
|
|
|
+- Encourages organized `.opencode/abilities/` directory structure
|
|
|
|
|
+
|
|
|
|
|
+### Debouncing
|
|
|
|
|
+- Ability detection limited to once per 10 seconds per ability
|
|
|
|
|
+- Prevents message spam from repeated ability suggestions
|
|
|
|
|
+- User can still manually run with `/ability.run`
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Extension Points
|
|
|
|
|
+
|
|
|
|
|
+### Adding New Step Types
|
|
|
|
|
+1. Add type definition to `types/index.ts`
|
|
|
|
|
+2. Add executor function in `executor/index.ts`
|
|
|
|
|
+3. Add to `ALLOWED_TOOLS_BY_STEP_TYPE`
|
|
|
|
|
+4. Update validator
|
|
|
|
|
+
|
|
|
|
|
+### Custom Validation
|
|
|
|
|
+1. Extend `validateAbility()` in `validator/index.ts`
|
|
|
|
|
+2. Add custom error messages
|
|
|
|
|
+3. Return enhanced validation result
|
|
|
|
|
+
|
|
|
|
|
+### Custom Tools
|
|
|
|
|
+1. Add tool definition in `opencode-plugin.ts`
|
|
|
|
|
+2. Implement execute function
|
|
|
|
|
+3. Register in tool map
|
|
|
|
|
+
|
|
|
|
|
+### Agent-Specific Abilities
|
|
|
|
|
+```yaml
|
|
|
|
|
+exclusive_agent: deploy-agent # Only this agent can run
|
|
|
|
|
+compatible_agents: [deploy-agent, devops-agent] # Whitelist
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Testing
|
|
|
|
|
+
|
|
|
|
|
+### Test Coverage (87 tests)
|
|
|
|
|
+- **executor.test.ts** - Step execution, dependencies, validation
|
|
|
|
|
+- **validator.test.ts** - Ability validation, input validation
|
|
|
|
|
+- **enforcement.test.ts** - Hook enforcement, agent attachment
|
|
|
|
|
+- **integration.test.ts** - Full lifecycle, error handling
|
|
|
|
|
+- **trigger.test.ts** - Keyword/pattern detection
|
|
|
|
|
+- **context-passing.test.ts** - Output context passing
|
|
|
|
|
+- **sdk.test.ts** - Public API
|
|
|
|
|
+
|
|
|
|
|
+### Running Tests
|
|
|
|
|
+```bash
|
|
|
|
|
+cd packages/plugin-abilities
|
|
|
|
|
+bun test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Troubleshooting
|
|
|
|
|
+
|
|
|
|
|
+### "Ability not found"
|
|
|
|
|
+- Check `.opencode/abilities/` directory exists
|
|
|
|
|
+- Check ability YAML file is valid
|
|
|
|
|
+- Run `ability.list` to see loaded abilities
|
|
|
|
|
+
|
|
|
|
|
+### "Input validation failed"
|
|
|
|
|
+- Check inputs match schema (type, pattern, enum, range)
|
|
|
|
|
+- Use `ability.validate <name>` to check definition
|
|
|
|
|
+
|
|
|
|
|
+### "Tool blocked during step"
|
|
|
|
|
+- Check enforcement mode (loose vs strict)
|
|
|
|
|
+- Tool not in `ALLOWED_TOOLS_BY_STEP_TYPE[stepType]`
|
|
|
|
|
+- Script steps block all tools (run deterministically)
|
|
|
|
|
+
|
|
|
|
|
+### "Step failed but continued"
|
|
|
|
|
+- Check `on_failure: continue` in step definition
|
|
|
|
|
+- Check `max_retries` configured
|
|
|
|
|
+
|
|
|
|
|
+### Plugin crashes OpenCode
|
|
|
|
|
+- Check hook signatures match SDK (`@opencode-ai/plugin`)
|
|
|
|
|
+- Ensure all hooks have try-catch blocks
|
|
|
|
|
+- Check console for error messages
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Design Philosophy
|
|
|
|
|
+
|
|
|
|
|
+### Why Enforcement?
|
|
|
|
|
+
|
|
|
|
|
+> **Hypothesis**: Traditional skills fail because LLMs are optimization engines, not planning engines. They optimize for "completion" not for "following instructions."
|
|
|
|
|
+
|
|
|
|
|
+**Solution**: Make it impossible to deviate
|
|
|
|
|
+- Block tools, not suggest them
|
|
|
|
|
+- Inject context, not hope it's remembered
|
|
|
|
|
+- Execute steps sequentially, not in parallel
|
|
|
|
|
+
|
|
|
|
|
+### Why Step-Based?
|
|
|
|
|
+
|
|
|
|
|
+> **Real World**: Complex tasks have dependencies and validation needs. Humans break them into steps for a reason.
|
|
|
|
|
+
|
|
|
|
|
+**Solution**: Explicit step dependencies
|
|
|
|
|
+- Test before build
|
|
|
|
|
+- Build before deploy
|
|
|
|
|
+- Approval before production
|
|
|
|
|
+
|
|
|
|
|
+### Why Validation?
|
|
|
|
|
+
|
|
|
|
|
+> **Problem**: Without validation, agents "guess" at outputs and continue. This causes silent failures.
|
|
|
|
|
+
|
|
|
|
|
+**Solution**: Assert expectations after each step
|
|
|
|
|
+- Script validation (exit codes, output content)
|
|
|
|
|
+- Input validation (required, pattern, range)
|
|
|
|
|
+- Dependency validation (no circular loops)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Summary
|
|
|
|
|
+
|
|
|
|
|
+The Abilities Plugin enforces deterministic, step-by-step workflow execution through:
|
|
|
|
|
+
|
|
|
|
|
+1. **Discovery** (loader) - Find ability definitions from YAML
|
|
|
|
|
+2. **Validation** (validator) - Ensure well-formed and valid inputs
|
|
|
|
|
+3. **Execution** (executor) - Run steps sequentially with context passing
|
|
|
|
|
+4. **Enforcement** (plugin hooks) - Block tools, inject context, track state
|
|
|
|
|
+5. **State Management** (ExecutionManager) - Lifecycle, cleanup, memory management
|
|
|
|
|
+
|
|
|
|
|
+Result: Agents follow prescribed workflows reliably, reproducibly, and safely.
|