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.
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
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)
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
Responsibility: Interface between OpenCode and the abilities system
Key Functions:
AbilitiesPlugin - Main async factory function that returns hooksmatchesTrigger() - Detect if user text matches ability keywords/patternsdetectAbility() - Find matching ability from user messageshowToast() - Display UI notificationscreateExecutorContext() - Build execution environmentbuildAbilityContextInjection() - Format ability status for agentHooks Implemented:
{
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:
Responsibility: Find and parse YAML ability definitions from filesystem
Key Functions:
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):
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?
Output:
Map<string, LoadedAbility> {
'deploy': { ability, filePath, source }
'deploy/staging': { ability, filePath, source }
'test-suite': { ability, filePath, source }
}
Responsibility: Ensure abilities are well-formed and inputs are valid
Key Functions:
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:
{
valid: boolean
errors: Array<{
path: string // e.g., "inputs.version"
message: string // "Must match pattern: ^v\d+\.\d+\.\d+$"
}>
}
Responsibility: Execute ability steps sequentially with dependency management
Key Functions:
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:
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:
steps:
- run: "deploy {{inputs.version}} to {{inputs.env}}"
- run: "echo {{steps.test.output}}" # From previous step output
Dependency Resolution:
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
Responsibility: Track active executions, manage state, handle cleanup
Key Responsibilities:
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:
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:
unref() so it doesn't prevent process exitResponsibility: Provide TypeScript types for all data structures
Key Types:
// 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
}
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..."
/ability.run deploy version=v1.2.3ability.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]
}
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
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
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
Set status: "completed"
Save results: { completedSteps: [...], duration: "42.3s" }
Return: "✅ Ability 'deploy' completed successfully"
Clear activeExecution for next ability
Problem: Agent might try to call bash during a script step (redundant & risky)
Solution:
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
Problem: Agent might forget which step it's on or what to do next
Solution:
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
Problem: User doesn't know they can run an ability
Solution:
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
.opencode/opencode.json:{
"plugin": [
"file://../packages/plugin-abilities/src/opencode-plugin.ts"
]
}
{
"abilities": {
"enabled": true,
"auto_trigger": true,
"enforcement": "strict",
"directories": [
".opencode/abilities",
"~/.config/opencode/abilities"
]
}
}
# .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
Lists all available abilities
ability.list
→ "- deploy: Deploy application...\n- test: Run tests..."
Execute an ability
ability.run { name: "deploy", inputs: { version: "v1.2.3" } }
→ { status: "completed", ability: "deploy", result: "..." }
Check active ability execution
ability.status
→ { status: "running", ability: "deploy", currentStep: "build", progress: "2/3" }
Cancel active ability
ability.cancel
→ { status: "cancelled", message: "Ability cancelled" }
┌──────────────────────────────────────────────────────────┐
│ 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
unref() so it doesn't block process exit*/*/ability.yaml).opencode/abilities/ directory structure/ability.runtypes/index.tsexecutor/index.tsALLOWED_TOOLS_BY_STEP_TYPEvalidateAbility() in validator/index.tsopencode-plugin.tsexclusive_agent: deploy-agent # Only this agent can run
compatible_agents: [deploy-agent, devops-agent] # Whitelist
cd packages/plugin-abilities
bun test
.opencode/abilities/ directory existsability.list to see loaded abilitiesability.validate <name> to check definitionALLOWED_TOOLS_BY_STEP_TYPE[stepType]on_failure: continue in step definitionmax_retries configured@opencode-ai/plugin)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
Real World: Complex tasks have dependencies and validation needs. Humans break them into steps for a reason.
Solution: Explicit step dependencies
Problem: Without validation, agents "guess" at outputs and continue. This causes silent failures.
Solution: Assert expectations after each step
The Abilities Plugin enforces deterministic, step-by-step workflow execution through:
Result: Agents follow prescribed workflows reliably, reproducibly, and safely.