opencode-agent-loop
v0.1.0
Published
Goal-oriented agent loop plugin for OpenCode — iteratively drives agents toward objectives with configurable budgets and termination conditions
Readme
opencode-agent-loop
Goal-oriented agent loop plugin for OpenCode — iteratively drives agents toward objectives with configurable budgets and termination conditions.
Quick Start
1. Register the plugin
In your opencode.json:
{
"plugins": [
["file:///path/to/opencode-agent-loop/dist/index.js"]
]
}2. Create a rule
Drop a JSON file named rule_<name>.json in ~/.config/opencode/agent-loop/ (e.g. rule_code-review.json):
{
"objective": "Review the code and fix all issues",
"doneWhen": [
{ "type": "marker", "value": "[GOAL_COMPLETE]", "where": "assistant-end" }
],
"budget": { "maxTurns": 5, "maxMinutes": 10 },
"enabled": true
}The rule name is derived from the filename: rule_code-review.json → name code-review.
Rules declare what the agent should achieve, but the loop does not start automatically. You must use the /loop slash command to activate it.
3. Use the /loop slash command
/loop start [--rule <name>] [goal] [--turns N] [--mins M] [-- <user_request>]
/loop stop
/loop status
/loop list| Argument | Description |
|----------|-------------|
| --rule <name> | Reference a rule by name (derived from rule_<name>.json filename) |
| goal | Overrides the rule's objective text. Used only in nudge messages, never sent directly to the LLM. |
| --turns N | Override budget.maxTurns for this run |
| --mins M | Override budget.maxMinutes for this run (also accepts --minutes) |
| -- <user_request> | After --, everything that follows is the actual user message sent to the LLM to kick off the work |
Examples
# Start with a rule, send a task to the LLM
/loop start --rule code-review -- Review the diff and fix all issues
# Start with a custom goal overriding the rule's objective
/loop start --rule code-review "Build a test suite" -- Write tests for src/utils
# Start with turn limit override
/loop start --rule code-review --turns 10
# Start without a rule (dynamic goal) — requires goal text and/or budget
/loop start "Fix all TypeScript errors" --turns 8 --mins 15 -- Fix the build errors
# List all available rules
/loop list
# Stop the running loop
/loop stop
# Check loop status
/loop statusUsage Scenarios
Below are practical scenarios showing how to use the agent loop plugin for different tasks.
Scenario 1: Code Review with Marker Completion
Create ~/.config/opencode/agent-loop/rule_code-review.json:
{
"objective": "Review all changed files and fix any bugs, style issues, or security vulnerabilities",
"doneWhen": [
{ "type": "marker", "value": "[REVIEW_DONE]", "where": "assistant-end" }
],
"budget": { "maxTurns": 3, "maxMinutes": 10 },
"enabled": true
}Then in chat:
/loop start --rule code-review "Review the latest commit" --turns 5 -- Review the diff and fix all issuesThe loop nudges the agent after each idle period, and terminates when [REVIEW_DONE] appears at the end of an assistant reply, or after 5 turns / 10 minutes.
Scenario 2: Budget-Only Loop (No Marker)
For tasks where completion isn't easily detectable, rely solely on budget:
{
"objective": "Refactor the payment module to use the new API",
"doneWhen": [],
"budget": { "maxTurns": 8, "maxMinutes": 30 },
"enabled": true
}Save as ~/.config/opencode/agent-loop/rule_refactor-module.json. Start with /loop start --rule refactor-module -- <task>.
The loop runs exactly 8 iterations (or 30 minutes) then forces an exhaust nudge.
Scenario 3: Quick Ad-Hoc Task (No Rule File)
Start a loop without any rule file — the goal, budget, and task are all on the command line:
/loop start "Add unit tests for the auth middleware" --turns 6 --mins 15 -- Write comprehensive tests in src/auth/__tests__This creates a dynamic goal with:
- Objective:
"Add unit tests for the auth middleware"(used in nudge messages) - Budget: 6 turns / 15 minutes
- Default
[GOAL_COMPLETE]marker (auto-added when budget is not provided) - The text after
--is sent directly to the LLM as the first user message
Scenario 4: Multiple Rules
Multiple rule files for different tasks:
~/.config/opencode/agent-loop/rule_write-docs.json:
{
"objective": "Write API documentation for all public functions",
"doneWhen": [{ "type": "marker", "value": "[DOCS_DONE]", "where": "assistant-end" }],
"budget": { "maxTurns": 10 }
}~/.config/opencode/agent-loop/rule_write-tests.json:
{
"objective": "Achieve 90% test coverage for the utils package",
"doneWhen": [{ "type": "regex", "pattern": "coverage:?\\s*9[0-9]%", "flags": "i" }],
"budget": { "maxTurns": 15, "maxMinutes": 20 }
}Use /loop start --rule write-docs or /loop start --rule write-tests in any agent session.
Scenario 5: Regex Completion Detection
Use a regex pattern instead of a marker:
{
"objective": "Fix all TypeScript compilation errors",
"doneWhen": [
{ "type": "regex", "pattern": "0\\s+errors?|all\\s+clear|compilation\\s+succeeded", "flags": "i" }
],
"budget": { "maxTurns": 5 }
}Save as ~/.config/opencode/agent-loop/rule_build-fix.json. The loop completes when any assistant reply matches the regex pattern.
Scenario 6: Time-Boxed Research Task
Use maxMinutes to enforce a strict time budget:
{
"objective": "Research and compare 3 Rust web frameworks (Actix, Axum, Rocket)",
"doneWhen": [
{ "type": "marker", "value": "[RESEARCH_COMPLETE]", "where": "assistant-end" }
],
"budget": { "maxMinutes": 15 },
"enabled": true
}/loop start --rule tech-research "Research Rust web frameworks" --mins 10 -- Compare Actix, Axum, and Rocket for our next projectAfter 10 minutes (or 15 from the rule), the time-exhaust nudge fires and the agent must deliver its final answer.
Scenario 7: Inline Rules via opencode.json
For portable setups, embed rules directly in opencode.json:
{
"plugins": [
[
"file:///path/to/opencode-agent-loop/dist/index.js",
{
"rules": [
{
"name": "daily-standup",
"objective": "Summarize today's commits and open issues from the project board",
"doneWhen": [{ "type": "marker", "value": "[STANDUP_DONE]", "where": "assistant-any" }],
"budget": { "maxTurns": 2 }
},
{
"name": "security-scan",
"objective": "Scan dependencies for known vulnerabilities and suggest fixes",
"doneWhen": [{ "type": "regex", "pattern": "vulnerabilit(y|ies).*(0|none|fixed)", "flags": "i" }],
"budget": { "maxTurns": 3, "maxMinutes": 5 }
}
],
"enableLogging": true
}
]
]
}Use /loop start --rule daily-standup to activate.
Scenario 8: Silent Loop — Let the Agent Work Independently
Start a loop with no initial user message — the agent works autonomously, nudged whenever idle:
/loop start --rule refactor-moduleNo -- argument means no user message is sent. The loop sits idle until the agent becomes idle, then injects the nudge.
Scenario 9: Stop and Resume
Stop a running loop:
/loop stopThe loop is cancelled. When you send a new user message in the same session, the loop automatically resumes (status flips from cancelled to running), so you can continue iterating.
Scenario 10: Check Loop Status & List Rules
Monitor the current loop state:
/loop statusExample output (via toast): Status: running | Goal: "Fix all TypeScript errors" | Iteration: 3/8 | Elapsed: 5min/15min
List all available rules:
/loop listExample output:
Available rules:
code-review
refactor-module
write-docs
write-testsPriority (highest to lowest)
| Setting | /loop args | Rule JSON | Global default |
|---------|-------------|-----------|----------------|
| Rule selection | --rule <name> | filename rule_<name>.json | — |
| objective (nudge text) | goal | objective | — |
| maxTurns | --turns N | budget.maxTurns | — |
| maxMinutes | --mins M | budget.maxMinutes | defaultMaxMinutes |
| doneWhen | — | doneWhen | [GOAL_COMPLETE] marker |
If you run /loop start with no --rule and no goal, it will reject with an error. Provide a goal or use --rule to reference a rule.
Configuration
Rule Schema
{
"objective": "Review the code changes and provide feedback",
"doneWhen": [
{ "type": "marker", "value": "[GOAL_COMPLETE]", "where": "assistant-end" }
],
"budget": {
"maxTurns": 5,
"maxMinutes": 10
},
"idleDelayMs": 3000,
"cooldownMs": 10000,
"enabled": true
}Rule Options
| Field | Type | Description |
|-------|------|-------------|
| objective | string | Goal text used in nudge messages (required) |
| doneWhen | DoneCondition[] | Termination conditions (default: [GOAL_COMPLETE] marker) |
| budget.maxTurns | number | Max nudge iterations before forced completion |
| budget.maxMinutes | number | Max time before forced completion |
| idleDelayMs | number | Delay before nudging an idle agent (default: read from config.json, fallback 3000) |
| cooldownMs | number | Minimum interval between nudges (default: read from config.json, fallback 10000) |
| enabled | boolean | Whether the rule is available (default: true). When enabled, the loop must still be activated with /loop start. |
The rule name is derived from the filename: ~/.config/opencode/agent-loop/rule_my-task.json → name my-task.
Done Condition Types
Marker — checks for a specific text in assistant output:
{ "type": "marker", "value": "[GOAL_COMPLETE]", "where": "assistant-end" }where: "assistant-end"— matches only at the end of assistant textwhere: "assistant-any"— matches anywhere in assistant text
Regex — matches against assistant text:
{ "type": "regex", "pattern": "review\\s+complete", "flags": "i" }Global Config
Create ~/.config/opencode/agent-loop/config.json for global defaults:
{
"idleDelayMs": 5000,
"cooldownMs": 15000,
"enableLogging": true,
"defaultMaxMinutes": 30
}These values are loaded from config.json and serve as fallbacks when not specified in the rule or /loop arguments.
Inline Rules
Pass rules directly in opencode.json:
{
"plugins": [
[
"file:///path/to/opencode-agent-loop/dist/index.js",
{
"rules": [
{
"name": "daily-standup",
"objective": "Summarize today's commits and open issues",
"budget": { "maxTurns": 3 }
}
],
"enableLogging": true
}
]
]
}Inline rules require an explicit name field (not derived from filename).
Development
npm install
npm run dev # watch mode
npm run build # production build
npm run test # run tests
npm run lint # lint
npm run typecheck # type checkLicense
MIT
