opencode-auto-loop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/commands/auto-loop-help.md +36 -0
- package/commands/auto-loop.md +55 -0
- package/commands/cancel-auto-loop.md +23 -0
- package/package.json +38 -0
- package/skills/auto-loop/SKILL.md +116 -0
- package/skills/auto-loop-help/SKILL.md +76 -0
- package/skills/cancel-auto-loop/SKILL.md +42 -0
- package/src/index.ts +596 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tim Lang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# opencode-auto-loop
|
|
2
|
+
|
|
3
|
+
Auto Loop plugin for [opencode](https://opencode.ai) — auto-continues until task completion.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your `~/.config/opencode/opencode.json`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"plugin": ["opencode-auto-loop"]
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Restart opencode. That's it!
|
|
16
|
+
|
|
17
|
+
On first run, the plugin will automatically install skills and commands to your `~/.config/opencode/` directory.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Start a loop
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
/auto-loop "Build a REST API with authentication"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The AI will work on your task and automatically continue until completion.
|
|
28
|
+
|
|
29
|
+
### Cancel a loop
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/cancel-auto-loop
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Get help
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
/auto-loop-help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
1. `/auto-loop` creates a state file at `.opencode/auto-loop.local.md`
|
|
44
|
+
2. When the AI goes idle, the plugin checks if `<promise>DONE</promise>` was output
|
|
45
|
+
3. If not found, it extracts progress (## Completed / ## Next Steps) and injects a continuation prompt
|
|
46
|
+
4. Loop continues until DONE is found or max iterations (100) reached
|
|
47
|
+
5. State file is deleted when complete
|
|
48
|
+
6. Loop context survives session compaction
|
|
49
|
+
|
|
50
|
+
### Progress Tracking
|
|
51
|
+
|
|
52
|
+
The plugin extracts `## Completed` and `## Next Steps` sections from each iteration and persists them in the state file. On continuation, these are included in the prompt so the AI knows exactly where to pick up.
|
|
53
|
+
|
|
54
|
+
### Completion Promise
|
|
55
|
+
|
|
56
|
+
When the AI finishes a task, it outputs:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
<promise>DONE</promise>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The AI should ONLY output this when the task is COMPLETELY and VERIFIABLY finished.
|
|
63
|
+
|
|
64
|
+
## State File
|
|
65
|
+
|
|
66
|
+
The loop state is stored in your project directory:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
.opencode/auto-loop.local.md
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Format (markdown with YAML frontmatter):
|
|
73
|
+
|
|
74
|
+
```markdown
|
|
75
|
+
---
|
|
76
|
+
active: true
|
|
77
|
+
iteration: 3
|
|
78
|
+
maxIterations: 100
|
|
79
|
+
sessionId: ses_abc123
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
Your original task prompt
|
|
83
|
+
|
|
84
|
+
## Completed
|
|
85
|
+
- [x] Set up project structure
|
|
86
|
+
- [x] Created database schema
|
|
87
|
+
|
|
88
|
+
## Next Steps
|
|
89
|
+
- [ ] Add JWT authentication middleware
|
|
90
|
+
- [ ] Create registration endpoint
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Add `.opencode/auto-loop.local.md` to your `.gitignore`.
|
|
94
|
+
|
|
95
|
+
## Features
|
|
96
|
+
|
|
97
|
+
- **Plug-and-play**: Just add to config and restart
|
|
98
|
+
- **Auto-setup**: Skills and commands are automatically installed on first run
|
|
99
|
+
- **Progress tracking**: Extracts and persists TODOs across iterations
|
|
100
|
+
- **Compaction-safe**: Loop context survives session compaction
|
|
101
|
+
- **Project-relative**: State file in `.opencode/`, not global
|
|
102
|
+
- **Completion detection**: Scans session messages for DONE promise (ignores code fences)
|
|
103
|
+
- **Toast notifications**: Visual feedback on loop start, iteration, completion
|
|
104
|
+
- **Error handling**: Pauses on session errors, cleans up on session deletion
|
|
105
|
+
- **Debounced**: Prevents duplicate continuations from rapid idle events
|
|
106
|
+
- **Commands**: `/auto-loop`, `/cancel-auto-loop`, and `/auto-loop-help`
|
|
107
|
+
|
|
108
|
+
## Architecture
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
opencode-auto-loop/
|
|
112
|
+
├── src/
|
|
113
|
+
│ └── index.ts # Main plugin with event hooks and tools
|
|
114
|
+
├── skills/
|
|
115
|
+
│ ├── auto-loop/ # Progressive context for starting loops
|
|
116
|
+
│ ├── cancel-auto-loop/ # Context for cancellation
|
|
117
|
+
│ └── help/ # Plugin documentation
|
|
118
|
+
├── commands/
|
|
119
|
+
│ ├── auto-loop.md # Slash command for starting
|
|
120
|
+
│ ├── cancel-auto-loop.md # Slash command for cancelling
|
|
121
|
+
│ └── help.md # Slash command for help
|
|
122
|
+
├── tsconfig.json
|
|
123
|
+
└── package.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show Auto Loop plugin help and available commands
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Auto Loop Help
|
|
6
|
+
|
|
7
|
+
## Available Commands
|
|
8
|
+
|
|
9
|
+
- `/auto-loop <task>` - Start an auto-continuation loop for the given task
|
|
10
|
+
- `/cancel-auto-loop` - Stop an active Auto Loop
|
|
11
|
+
- `/auto-loop-help` - Show this help
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
/auto-loop Build a REST API with user authentication
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The AI will work on your task and automatically continue until it outputs `<promise>DONE</promise>` to signal completion.
|
|
20
|
+
|
|
21
|
+
## How It Works
|
|
22
|
+
|
|
23
|
+
1. Creates state file at `.opencode/auto-loop.local.md`
|
|
24
|
+
2. Works on task until idle
|
|
25
|
+
3. If no `<promise>DONE</promise>` found, auto-continues
|
|
26
|
+
4. Repeats until complete or max iterations (100) reached
|
|
27
|
+
5. Survives context compaction — loop state is injected into the summary
|
|
28
|
+
|
|
29
|
+
## Cancellation
|
|
30
|
+
|
|
31
|
+
To stop early:
|
|
32
|
+
```
|
|
33
|
+
/cancel-auto-loop
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For more details, the AI can use the `auto-loop-help` tool.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Start Auto Loop - auto-continues until task completion
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Auto Loop
|
|
6
|
+
|
|
7
|
+
Start an iterative development loop that automatically continues until the task is complete.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Create the state file in the project directory:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
mkdir -p .opencode && cat > .opencode/auto-loop.local.md << 'EOF'
|
|
15
|
+
---
|
|
16
|
+
active: true
|
|
17
|
+
iteration: 0
|
|
18
|
+
maxIterations: 100
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
$ARGUMENTS
|
|
22
|
+
EOF
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Task
|
|
26
|
+
|
|
27
|
+
Now begin working on the task: **$ARGUMENTS**
|
|
28
|
+
|
|
29
|
+
## Progress Tracking
|
|
30
|
+
|
|
31
|
+
Before going idle, you MUST output structured progress so the plugin knows where you left off:
|
|
32
|
+
|
|
33
|
+
```markdown
|
|
34
|
+
## Completed
|
|
35
|
+
- [x] What you finished this iteration
|
|
36
|
+
|
|
37
|
+
## Next Steps
|
|
38
|
+
- [ ] What needs to be done next (in priority order)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The plugin extracts these into the state file for the next iteration's continuation prompt.
|
|
42
|
+
|
|
43
|
+
## Completion
|
|
44
|
+
|
|
45
|
+
When the task is FULLY completed, signal completion by outputting:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
<promise>DONE</promise>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**IMPORTANT:** ONLY output this when the task is COMPLETELY and VERIFIABLY finished. Do NOT output false promises to escape the loop.
|
|
52
|
+
|
|
53
|
+
## Cancellation
|
|
54
|
+
|
|
55
|
+
Use `/cancel-auto-loop` to stop early.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Cancel active Auto Loop
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Cancel Loop
|
|
6
|
+
|
|
7
|
+
Cancel the active Auto Loop.
|
|
8
|
+
|
|
9
|
+
## Steps
|
|
10
|
+
|
|
11
|
+
1. Check if a loop is active and get the iteration count:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
if [ -f .opencode/auto-loop.local.md ]; then
|
|
15
|
+
grep '^iteration:' .opencode/auto-loop.local.md
|
|
16
|
+
rm -f .opencode/auto-loop.local.md
|
|
17
|
+
echo "Auto Loop cancelled."
|
|
18
|
+
else
|
|
19
|
+
echo "No active Auto Loop to cancel."
|
|
20
|
+
fi
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. Report the result to the user.
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-auto-loop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Auto-continue for OpenCode",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"opencode",
|
|
9
|
+
"opencode-plugin",
|
|
10
|
+
"auto-continue",
|
|
11
|
+
"auto-loop",
|
|
12
|
+
"iteration-loop"
|
|
13
|
+
],
|
|
14
|
+
"author": "Tim Lang",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/timmyjl12/opencode-auto-loop"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/timmyjl12/opencode-auto-loop/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/timmyjl12/opencode-auto-loop#readme",
|
|
24
|
+
"files": [
|
|
25
|
+
"src/",
|
|
26
|
+
"commands/",
|
|
27
|
+
"skills/"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@opencode-ai/plugin": "^0.15.31"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.5.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auto-loop
|
|
3
|
+
description: Start Auto Loop - auto-continues until task completion
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Auto Loop
|
|
7
|
+
|
|
8
|
+
Start an iterative development loop that automatically continues until the task is complete.
|
|
9
|
+
|
|
10
|
+
## How It Works
|
|
11
|
+
|
|
12
|
+
The Auto Loop creates a continuous feedback cycle for completing complex tasks:
|
|
13
|
+
|
|
14
|
+
1. You work on the task until you go idle
|
|
15
|
+
2. The plugin detects the idle state and checks for completion
|
|
16
|
+
3. If not complete, it extracts your progress and prompts you to continue
|
|
17
|
+
4. This repeats until you output the completion promise or max iterations reached
|
|
18
|
+
|
|
19
|
+
Your previous work remains accessible through files, git history, and the state file's progress sections.
|
|
20
|
+
|
|
21
|
+
## Starting the Loop
|
|
22
|
+
|
|
23
|
+
When you invoke this skill, create the state file in the project directory:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
mkdir -p .opencode && cat > .opencode/auto-loop.local.md << 'EOF'
|
|
27
|
+
---
|
|
28
|
+
active: true
|
|
29
|
+
iteration: 0
|
|
30
|
+
maxIterations: 100
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
[The user's task prompt goes here]
|
|
34
|
+
EOF
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then inform the user and begin working on the task.
|
|
38
|
+
|
|
39
|
+
## Progress Tracking - CRITICAL
|
|
40
|
+
|
|
41
|
+
**Before going idle at the end of each work session, you MUST output structured progress sections.** The plugin parses these to persist your TODOs across iterations so you know exactly where to pick up.
|
|
42
|
+
|
|
43
|
+
Use this format in your final message of each iteration:
|
|
44
|
+
|
|
45
|
+
```markdown
|
|
46
|
+
## Completed
|
|
47
|
+
- [x] Set up project structure
|
|
48
|
+
- [x] Created database schema
|
|
49
|
+
- [x] Implemented user model
|
|
50
|
+
|
|
51
|
+
## Next Steps
|
|
52
|
+
- [ ] Add JWT authentication middleware
|
|
53
|
+
- [ ] Create registration endpoint
|
|
54
|
+
- [ ] Write integration tests
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Rules:**
|
|
58
|
+
- Always use checkbox format (`- [x]` for done, `- [ ]` for remaining)
|
|
59
|
+
- Be specific — each item should be a concrete, actionable step
|
|
60
|
+
- Only list truly completed items under ## Completed
|
|
61
|
+
- Order ## Next Steps by priority — the continuation will tell you to start from the top
|
|
62
|
+
- The plugin extracts these sections and writes them into `auto-loop.local.md` for the next iteration
|
|
63
|
+
|
|
64
|
+
## Completion Promise - CRITICAL RULES
|
|
65
|
+
|
|
66
|
+
When you have FULLY completed the task, signal completion by outputting:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
<promise>DONE</promise>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**IMPORTANT CONSTRAINTS:**
|
|
73
|
+
|
|
74
|
+
- ONLY output `<promise>DONE</promise>` when the task is COMPLETELY and VERIFIABLY finished
|
|
75
|
+
- The statement MUST be completely and unequivocally TRUE
|
|
76
|
+
- Do NOT output false promises to escape the loop, even if you think you're stuck
|
|
77
|
+
- Do NOT lie even if you think you should exit for other reasons
|
|
78
|
+
- If you're blocked, explain the blocker and request help instead of falsely completing
|
|
79
|
+
|
|
80
|
+
The loop can only be stopped by:
|
|
81
|
+
1. Truthful completion promise
|
|
82
|
+
2. Max iterations reached
|
|
83
|
+
3. User running `/cancel-auto-loop`
|
|
84
|
+
|
|
85
|
+
## Checking Status
|
|
86
|
+
|
|
87
|
+
Check current iteration and progress:
|
|
88
|
+
```bash
|
|
89
|
+
cat .opencode/auto-loop.local.md
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## State File Format
|
|
93
|
+
|
|
94
|
+
The state file at `.opencode/auto-loop.local.md` uses YAML frontmatter with progress sections:
|
|
95
|
+
|
|
96
|
+
```markdown
|
|
97
|
+
---
|
|
98
|
+
active: true
|
|
99
|
+
iteration: 3
|
|
100
|
+
maxIterations: 100
|
|
101
|
+
sessionId: ses_abc123
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Build a REST API with authentication
|
|
105
|
+
|
|
106
|
+
## Completed
|
|
107
|
+
- [x] Set up project structure
|
|
108
|
+
- [x] Created database schema
|
|
109
|
+
|
|
110
|
+
## Next Steps
|
|
111
|
+
- [ ] Add JWT authentication middleware
|
|
112
|
+
- [ ] Create registration endpoint
|
|
113
|
+
- [ ] Write integration tests
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Add `.opencode/auto-loop.local.md` to your `.gitignore`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auto-loop-help
|
|
3
|
+
description: Explain Auto Loop plugin and available commands
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Auto Loop Help
|
|
7
|
+
|
|
8
|
+
The Auto Loop plugin provides auto-continuation for complex tasks in opencode.
|
|
9
|
+
|
|
10
|
+
## Available Commands
|
|
11
|
+
|
|
12
|
+
### `/auto-loop <task>`
|
|
13
|
+
Start an iterative development loop that automatically continues until the task is complete.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
```
|
|
17
|
+
/auto-loop Build a REST API with authentication
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The AI will work on your task and automatically continue until completion.
|
|
21
|
+
|
|
22
|
+
### `/cancel-auto-loop`
|
|
23
|
+
Cancel an active Auto Loop before it completes.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
```
|
|
27
|
+
/cancel-auto-loop
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `/auto-loop-help`
|
|
31
|
+
Show plugin help and available commands.
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
1. **Start**: `/auto-loop` creates a state file at `.opencode/auto-loop.local.md`
|
|
36
|
+
2. **Loop**: When the AI goes idle, the plugin checks if `<promise>DONE</promise>` was output
|
|
37
|
+
3. **Continue**: If not found, it injects "Continue from where you left off"
|
|
38
|
+
4. **Stop**: Loop continues until DONE is found or max iterations (100) reached
|
|
39
|
+
5. **Cleanup**: State file is deleted when complete
|
|
40
|
+
6. **Compaction**: Loop context survives session compaction — task and iteration info is preserved
|
|
41
|
+
|
|
42
|
+
## Completion Signal
|
|
43
|
+
|
|
44
|
+
When the task is fully complete, the AI outputs:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
<promise>DONE</promise>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This signals the loop to stop. The AI should ONLY output this when the task is truly complete.
|
|
51
|
+
|
|
52
|
+
## State File
|
|
53
|
+
|
|
54
|
+
Located at `.opencode/auto-loop.local.md` (add to `.gitignore`):
|
|
55
|
+
|
|
56
|
+
```markdown
|
|
57
|
+
---
|
|
58
|
+
active: true
|
|
59
|
+
iteration: 3
|
|
60
|
+
maxIterations: 100
|
|
61
|
+
sessionId: ses_abc123
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
Your original task prompt
|
|
65
|
+
|
|
66
|
+
## Completed
|
|
67
|
+
- [x] Set up project structure
|
|
68
|
+
|
|
69
|
+
## Next Steps
|
|
70
|
+
- [ ] Add authentication
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Credits
|
|
74
|
+
|
|
75
|
+
- Inspired by [Anthropic's auto-continue plugin](https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum) for Claude Code
|
|
76
|
+
- Based on [opencode-auto-loop](https://github.com/timmyjl12/opencode-auto-loop)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cancel-auto-loop
|
|
3
|
+
description: Cancel active Auto Loop
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cancel Loop
|
|
7
|
+
|
|
8
|
+
Stop an active Auto Loop before completion.
|
|
9
|
+
|
|
10
|
+
## How to Use
|
|
11
|
+
|
|
12
|
+
When you invoke this skill:
|
|
13
|
+
|
|
14
|
+
1. First, check if a loop is active:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
test -f .opencode/auto-loop.local.md && echo "Loop is active" || echo "No active loop"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. If active, read the current iteration count:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
grep '^iteration:' .opencode/auto-loop.local.md
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. Delete the state file to stop the loop:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
rm -f .opencode/auto-loop.local.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
4. Inform the user of the cancellation and which iteration was reached.
|
|
33
|
+
|
|
34
|
+
## When to Use
|
|
35
|
+
|
|
36
|
+
Use this command when:
|
|
37
|
+
- The task requirements have changed
|
|
38
|
+
- You want to restart with different parameters
|
|
39
|
+
- The loop appears stuck and you want manual control
|
|
40
|
+
- You need to work on something else
|
|
41
|
+
|
|
42
|
+
Note: Prefer completing tasks properly with `<promise>DONE</promise>` when possible.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { type Plugin, type PluginInput, tool } from "@opencode-ai/plugin";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
cpSync,
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
|
|
14
|
+
// Types
|
|
15
|
+
interface LoopState {
|
|
16
|
+
active: boolean;
|
|
17
|
+
iteration: number;
|
|
18
|
+
maxIterations: number;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
prompt?: string;
|
|
21
|
+
completed?: string;
|
|
22
|
+
nextSteps?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
26
|
+
type LogFn = (level: LogLevel, message: string) => void;
|
|
27
|
+
type OpencodeClient = PluginInput["client"];
|
|
28
|
+
|
|
29
|
+
// Constants
|
|
30
|
+
const SERVICE_NAME = "auto-loop";
|
|
31
|
+
const STATE_FILENAME = "auto-loop.local.md";
|
|
32
|
+
const OPENCODE_CONFIG_DIR = join(homedir(), ".config/opencode");
|
|
33
|
+
const COMPLETION_TAG = /<promise>\s*DONE\s*<\/promise>/is;
|
|
34
|
+
const DEBOUNCE_MS = 2000;
|
|
35
|
+
|
|
36
|
+
// Get plugin root directory (ESM only — package is "type": "module")
|
|
37
|
+
function getPluginRoot(): string {
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
return dirname(dirname(__filename)); // Go up from src/ to plugin root
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Content-aware copy: always update if content differs
|
|
43
|
+
function copyIfChanged(src: string, dest: string): void {
|
|
44
|
+
if (!existsSync(src)) return;
|
|
45
|
+
if (existsSync(dest)) {
|
|
46
|
+
const srcContent = readFileSync(src, "utf-8");
|
|
47
|
+
const destContent = readFileSync(dest, "utf-8");
|
|
48
|
+
if (srcContent === destContent) return;
|
|
49
|
+
}
|
|
50
|
+
const destDir = dirname(dest);
|
|
51
|
+
mkdirSync(destDir, { recursive: true });
|
|
52
|
+
cpSync(src, dest, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Auto-copy skills and commands to opencode config, updating if content changed
|
|
56
|
+
function setupSkillsAndCommands(log: LogFn): void {
|
|
57
|
+
const pluginRoot = getPluginRoot();
|
|
58
|
+
const skillsDir = join(OPENCODE_CONFIG_DIR, "skill");
|
|
59
|
+
const commandsDir = join(OPENCODE_CONFIG_DIR, "command");
|
|
60
|
+
|
|
61
|
+
// Copy skills
|
|
62
|
+
const pluginSkillsDir = join(pluginRoot, "skills");
|
|
63
|
+
if (existsSync(pluginSkillsDir)) {
|
|
64
|
+
const skills = ["auto-loop", "cancel-auto-loop", "auto-loop-help"];
|
|
65
|
+
for (const skill of skills) {
|
|
66
|
+
const srcFile = join(pluginSkillsDir, skill, "SKILL.md");
|
|
67
|
+
const destFile = join(skillsDir, skill, "SKILL.md");
|
|
68
|
+
try {
|
|
69
|
+
copyIfChanged(srcFile, destFile);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
log("warn", `Failed to copy skill '${skill}': ${err}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Copy commands
|
|
77
|
+
const pluginCommandsDir = join(pluginRoot, "commands");
|
|
78
|
+
if (existsSync(pluginCommandsDir)) {
|
|
79
|
+
const commands = ["auto-loop.md", "cancel-auto-loop.md", "auto-loop-help.md"];
|
|
80
|
+
for (const cmd of commands) {
|
|
81
|
+
try {
|
|
82
|
+
copyIfChanged(join(pluginCommandsDir, cmd), join(commandsDir, cmd));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log("warn", `Failed to copy command '${cmd}': ${err}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Get state file path (project-relative)
|
|
91
|
+
function getStateFile(directory: string): string {
|
|
92
|
+
return join(directory, ".opencode", STATE_FILENAME);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse markdown frontmatter state
|
|
96
|
+
function parseState(content: string): LoopState {
|
|
97
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
98
|
+
if (!match) return { active: false, iteration: 0, maxIterations: 100 };
|
|
99
|
+
|
|
100
|
+
const frontmatter = match[1];
|
|
101
|
+
const state: LoopState = {
|
|
102
|
+
active: false,
|
|
103
|
+
iteration: 0,
|
|
104
|
+
maxIterations: 100,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
for (const line of frontmatter.split("\n")) {
|
|
108
|
+
const [key, ...valueParts] = line.split(":");
|
|
109
|
+
const value = valueParts.join(":").trim();
|
|
110
|
+
if (key === "active") state.active = value === "true";
|
|
111
|
+
if (key === "iteration") state.iteration = parseInt(value) || 0;
|
|
112
|
+
if (key === "maxIterations") state.maxIterations = parseInt(value) || 100;
|
|
113
|
+
if (key === "sessionId") state.sessionId = value || undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get prompt and progress sections from body (after frontmatter)
|
|
117
|
+
const body = content.slice(match[0].length).trim();
|
|
118
|
+
if (body) {
|
|
119
|
+
// Split body at ## Completed / ## Next Steps section boundaries
|
|
120
|
+
const parts = body.split(/^(?=## (?:Completed|Next Steps))/m);
|
|
121
|
+
|
|
122
|
+
// First part is always the original prompt
|
|
123
|
+
state.prompt = parts[0].trim();
|
|
124
|
+
|
|
125
|
+
// Remaining parts are the progress sections
|
|
126
|
+
for (let i = 1; i < parts.length; i++) {
|
|
127
|
+
const section = parts[i];
|
|
128
|
+
if (section.startsWith("## Completed")) {
|
|
129
|
+
state.completed = section.replace(/^## Completed\n?/, "").trim();
|
|
130
|
+
} else if (section.startsWith("## Next Steps")) {
|
|
131
|
+
state.nextSteps = section.replace(/^## Next Steps\n?/, "").trim();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return state;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Serialize state to markdown frontmatter with progress sections
|
|
140
|
+
function serializeState(state: LoopState): string {
|
|
141
|
+
const lines = [
|
|
142
|
+
"---",
|
|
143
|
+
`active: ${state.active}`,
|
|
144
|
+
`iteration: ${state.iteration}`,
|
|
145
|
+
`maxIterations: ${state.maxIterations}`,
|
|
146
|
+
];
|
|
147
|
+
if (state.sessionId) lines.push(`sessionId: ${state.sessionId}`);
|
|
148
|
+
lines.push("---");
|
|
149
|
+
if (state.prompt) lines.push("", state.prompt);
|
|
150
|
+
if (state.completed) lines.push("", "## Completed", state.completed);
|
|
151
|
+
if (state.nextSteps) lines.push("", "## Next Steps", state.nextSteps);
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Read state from project directory
|
|
156
|
+
function readState(directory: string): LoopState {
|
|
157
|
+
const stateFile = getStateFile(directory);
|
|
158
|
+
if (existsSync(stateFile)) {
|
|
159
|
+
return parseState(readFileSync(stateFile, "utf-8"));
|
|
160
|
+
}
|
|
161
|
+
return { active: false, iteration: 0, maxIterations: 100 };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Write state to project directory
|
|
165
|
+
function writeState(directory: string, state: LoopState, log: LogFn): void {
|
|
166
|
+
try {
|
|
167
|
+
const stateFile = getStateFile(directory);
|
|
168
|
+
mkdirSync(dirname(stateFile), { recursive: true });
|
|
169
|
+
writeFileSync(stateFile, serializeState(state));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
log("error", `Failed to write state: ${err}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Clear state
|
|
176
|
+
function clearState(directory: string, log: LogFn): void {
|
|
177
|
+
try {
|
|
178
|
+
const stateFile = getStateFile(directory);
|
|
179
|
+
if (existsSync(stateFile)) unlinkSync(stateFile);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
log("warn", `Failed to clear state: ${err}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Strip markdown code fences before checking for completion tag
|
|
186
|
+
function stripCodeFences(text: string): string {
|
|
187
|
+
return text.replace(/```[\s\S]*?```/g, "");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Extract text from the last assistant message in a session
|
|
191
|
+
async function getLastAssistantText(
|
|
192
|
+
client: OpencodeClient,
|
|
193
|
+
sessionId: string,
|
|
194
|
+
directory: string,
|
|
195
|
+
log: LogFn
|
|
196
|
+
): Promise<string | null> {
|
|
197
|
+
try {
|
|
198
|
+
const response = await client.session.messages({
|
|
199
|
+
path: { id: sessionId },
|
|
200
|
+
query: { directory },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const messages = response.data ?? [];
|
|
204
|
+
|
|
205
|
+
const assistantMessages = messages.filter(
|
|
206
|
+
(msg) => msg.info?.role === "assistant"
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (assistantMessages.length === 0) return null;
|
|
210
|
+
|
|
211
|
+
const lastAssistant = assistantMessages[assistantMessages.length - 1];
|
|
212
|
+
const parts = lastAssistant.parts || [];
|
|
213
|
+
|
|
214
|
+
return parts
|
|
215
|
+
.filter((p) => p.type === "text")
|
|
216
|
+
.map((p) => ("text" in p ? p.text : "") ?? "")
|
|
217
|
+
.join("\n");
|
|
218
|
+
} catch (err) {
|
|
219
|
+
log("warn", `Failed to fetch session messages: ${err}`);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check completion by looking for <promise>DONE</promise> in last assistant text
|
|
225
|
+
function checkCompletion(text: string): boolean {
|
|
226
|
+
return COMPLETION_TAG.test(stripCodeFences(text));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Extract next steps / TODOs from assistant message text
|
|
230
|
+
// Looks for common patterns: ## Next Steps, ## TODO, checkbox lists, numbered lists after keywords
|
|
231
|
+
function extractNextSteps(text: string): string | undefined {
|
|
232
|
+
// Strategy 1: Look for an explicit ## Next Steps / ## TODO / ## Remaining section
|
|
233
|
+
const sectionMatch = text.match(
|
|
234
|
+
/^##\s*(?:Next Steps|TODO|Remaining|What's Left|Still To Do|Outstanding)[^\n]*\n([\s\S]*?)(?=\n## |\n\n---|$)/im
|
|
235
|
+
);
|
|
236
|
+
if (sectionMatch) {
|
|
237
|
+
const content = sectionMatch[1].trim();
|
|
238
|
+
if (content) return content;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Strategy 2: Collect all unchecked checkbox items (- [ ] ...)
|
|
242
|
+
const unchecked = text
|
|
243
|
+
.split("\n")
|
|
244
|
+
.filter((line) => /^\s*-\s*\[ \]/.test(line))
|
|
245
|
+
.map((line) => line.trim());
|
|
246
|
+
if (unchecked.length > 0) {
|
|
247
|
+
return unchecked.join("\n");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Strategy 3: Look for a numbered list after "next" or "todo" or "remaining" keywords
|
|
251
|
+
const numberedMatch = text.match(
|
|
252
|
+
/(?:next|todo|remaining|still need to|still to do)[^\n]*\n((?:\s*\d+\.\s+[^\n]+\n?)+)/i
|
|
253
|
+
);
|
|
254
|
+
if (numberedMatch) {
|
|
255
|
+
return numberedMatch[1].trim();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Extract completed items from assistant message text
|
|
262
|
+
function extractCompleted(text: string): string | undefined {
|
|
263
|
+
// Strategy 1: Look for an explicit ## Completed / ## Done / ## Progress section
|
|
264
|
+
const sectionMatch = text.match(
|
|
265
|
+
/^##\s*(?:Completed|Done|Progress|Accomplished|Finished)[^\n]*\n([\s\S]*?)(?=\n## |\n\n---|$)/im
|
|
266
|
+
);
|
|
267
|
+
if (sectionMatch) {
|
|
268
|
+
const content = sectionMatch[1].trim();
|
|
269
|
+
if (content) return content;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Strategy 2: Collect all checked checkbox items (- [x] ...)
|
|
273
|
+
const checked = text
|
|
274
|
+
.split("\n")
|
|
275
|
+
.filter((line) => /^\s*-\s*\[x\]/i.test(line))
|
|
276
|
+
.map((line) => line.trim());
|
|
277
|
+
if (checked.length > 0) {
|
|
278
|
+
return checked.join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Merge completed items: deduplicate by normalizing checkbox text
|
|
285
|
+
function mergeCompleted(
|
|
286
|
+
existing: string | undefined,
|
|
287
|
+
incoming: string | undefined
|
|
288
|
+
): string | undefined {
|
|
289
|
+
if (!existing && !incoming) return undefined;
|
|
290
|
+
if (!existing) return incoming;
|
|
291
|
+
if (!incoming) return existing;
|
|
292
|
+
|
|
293
|
+
const existingLines = existing.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
294
|
+
const incomingLines = incoming.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
295
|
+
|
|
296
|
+
// Normalize for dedup: strip checkbox prefix and lowercase
|
|
297
|
+
const normalize = (line: string) =>
|
|
298
|
+
line.replace(/^-\s*\[x\]\s*/i, "").trim().toLowerCase();
|
|
299
|
+
const existingSet = new Set(existingLines.map(normalize));
|
|
300
|
+
|
|
301
|
+
for (const line of incomingLines) {
|
|
302
|
+
if (!existingSet.has(normalize(line))) {
|
|
303
|
+
existingLines.push(line);
|
|
304
|
+
existingSet.add(normalize(line));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return existingLines.join("\n");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Build a progress section for the continuation prompt
|
|
312
|
+
function buildProgressSection(state: LoopState): string {
|
|
313
|
+
const sections: string[] = [];
|
|
314
|
+
|
|
315
|
+
if (state.completed) {
|
|
316
|
+
sections.push(`\n## Completed So Far\n${state.completed}`);
|
|
317
|
+
}
|
|
318
|
+
if (state.nextSteps) {
|
|
319
|
+
sections.push(`\n## Next Steps (pick up here)\n${state.nextSteps}`);
|
|
320
|
+
}
|
|
321
|
+
if (sections.length === 0) {
|
|
322
|
+
sections.push(
|
|
323
|
+
"\nNo structured progress recorded yet. Review your work so far and continue."
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return sections.join("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Build the loop context reminder for post-compaction injection
|
|
331
|
+
function buildLoopContextReminder(state: LoopState): string {
|
|
332
|
+
const progress = buildProgressSection(state);
|
|
333
|
+
return `[AUTO LOOP ACTIVE — Iteration ${state.iteration}/${state.maxIterations}]
|
|
334
|
+
|
|
335
|
+
Original task: ${state.prompt || "(no task specified)"}
|
|
336
|
+
${progress}
|
|
337
|
+
When the task is FULLY complete, you MUST output: <promise>DONE</promise>
|
|
338
|
+
Before going idle, list your progress using ## Completed and ## Next Steps sections.
|
|
339
|
+
Do NOT output false completion promises. If blocked, explain the blocker.`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Main plugin
|
|
343
|
+
export const AutoLoopPlugin: Plugin = async (ctx) => {
|
|
344
|
+
const directory = ctx.directory || process.cwd();
|
|
345
|
+
const client = ctx.client;
|
|
346
|
+
|
|
347
|
+
// Structured logger using the SDK's app.log API
|
|
348
|
+
const log: LogFn = (level, message) => {
|
|
349
|
+
try {
|
|
350
|
+
client.app.log({
|
|
351
|
+
body: { service: SERVICE_NAME, level, message },
|
|
352
|
+
});
|
|
353
|
+
} catch {
|
|
354
|
+
// Last resort: if logging itself fails, silently ignore
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Toast helper using the SDK's tui.showToast API
|
|
359
|
+
const toast = (message: string, variant: "info" | "success" | "warning" | "error" = "info") => {
|
|
360
|
+
try {
|
|
361
|
+
client.tui.showToast({
|
|
362
|
+
body: { message, variant },
|
|
363
|
+
});
|
|
364
|
+
} catch {
|
|
365
|
+
// Non-critical — ignore
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Auto-setup skills and commands
|
|
370
|
+
setupSkillsAndCommands(log);
|
|
371
|
+
|
|
372
|
+
// Debounce tracking for idle events
|
|
373
|
+
let lastContinuation = 0;
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
tool: {
|
|
377
|
+
"auto-loop": tool({
|
|
378
|
+
description:
|
|
379
|
+
"Start Auto Loop — auto-continues until task completion. Use: /auto-loop <task description>",
|
|
380
|
+
args: {
|
|
381
|
+
task: tool.schema
|
|
382
|
+
.string()
|
|
383
|
+
.describe("The task to work on until completion"),
|
|
384
|
+
maxIterations: tool.schema
|
|
385
|
+
.number()
|
|
386
|
+
.optional()
|
|
387
|
+
.describe("Maximum iterations (default: 100)"),
|
|
388
|
+
},
|
|
389
|
+
async execute({ task, maxIterations = 100 }, context) {
|
|
390
|
+
const state: LoopState = {
|
|
391
|
+
active: true,
|
|
392
|
+
iteration: 0,
|
|
393
|
+
maxIterations,
|
|
394
|
+
sessionId: context.sessionID,
|
|
395
|
+
prompt: task,
|
|
396
|
+
};
|
|
397
|
+
writeState(directory, state, log);
|
|
398
|
+
|
|
399
|
+
log("info", `Loop started for session ${context.sessionID}`);
|
|
400
|
+
toast(`Auto Loop started (max ${maxIterations} iterations)`, "success");
|
|
401
|
+
|
|
402
|
+
return `Auto Loop started (max ${maxIterations} iterations).
|
|
403
|
+
|
|
404
|
+
Task: ${task}
|
|
405
|
+
|
|
406
|
+
I will auto-continue until the task is complete. Before going idle each iteration, I will output structured progress:
|
|
407
|
+
|
|
408
|
+
\`\`\`
|
|
409
|
+
## Completed
|
|
410
|
+
- [x] What I finished
|
|
411
|
+
|
|
412
|
+
## Next Steps
|
|
413
|
+
- [ ] What remains (in priority order)
|
|
414
|
+
\`\`\`
|
|
415
|
+
|
|
416
|
+
When fully done, I will output \`<promise>DONE</promise>\` to signal completion.
|
|
417
|
+
|
|
418
|
+
Use /cancel-auto-loop to stop early.`;
|
|
419
|
+
},
|
|
420
|
+
}),
|
|
421
|
+
|
|
422
|
+
"cancel-auto-loop": tool({
|
|
423
|
+
description: "Cancel active Auto Loop",
|
|
424
|
+
args: {},
|
|
425
|
+
async execute() {
|
|
426
|
+
const state = readState(directory);
|
|
427
|
+
if (!state.active) {
|
|
428
|
+
return "No active Auto Loop to cancel.";
|
|
429
|
+
}
|
|
430
|
+
const iterations = state.iteration;
|
|
431
|
+
clearState(directory, log);
|
|
432
|
+
|
|
433
|
+
log("info", `Loop cancelled after ${iterations} iteration(s)`);
|
|
434
|
+
toast(`Auto Loop cancelled after ${iterations} iteration(s)`, "warning");
|
|
435
|
+
|
|
436
|
+
return `Auto Loop cancelled after ${iterations} iteration(s).`;
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
439
|
+
|
|
440
|
+
"auto-loop-help": tool({
|
|
441
|
+
description: "Show Auto Loop plugin help",
|
|
442
|
+
args: {},
|
|
443
|
+
async execute() {
|
|
444
|
+
return `# Auto Loop Help
|
|
445
|
+
|
|
446
|
+
## Available Commands
|
|
447
|
+
|
|
448
|
+
- \`/auto-loop <task>\` - Start an auto-continuation loop
|
|
449
|
+
- \`/cancel-auto-loop\` - Stop an active loop
|
|
450
|
+
- \`/auto-loop-help\` - Show this help
|
|
451
|
+
|
|
452
|
+
## How It Works
|
|
453
|
+
|
|
454
|
+
1. Start with: /auto-loop "Build a REST API"
|
|
455
|
+
2. AI works on the task until idle
|
|
456
|
+
3. Plugin auto-continues if not complete
|
|
457
|
+
4. Loop stops when AI outputs: <promise>DONE</promise>
|
|
458
|
+
|
|
459
|
+
## State File
|
|
460
|
+
|
|
461
|
+
Located at: .opencode/auto-loop.local.md`;
|
|
462
|
+
},
|
|
463
|
+
}),
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
// Event hooks for auto-continuation, compaction recovery, and lifecycle
|
|
467
|
+
event: async ({ event }) => {
|
|
468
|
+
// --- session.idle: core auto-continuation logic ---
|
|
469
|
+
if (event.type === "session.idle") {
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
if (now - lastContinuation < DEBOUNCE_MS) return;
|
|
472
|
+
|
|
473
|
+
const sessionId = event.properties.sessionID;
|
|
474
|
+
const state = readState(directory);
|
|
475
|
+
|
|
476
|
+
if (!state.active) return;
|
|
477
|
+
if (!sessionId) return;
|
|
478
|
+
if (state.sessionId && state.sessionId !== sessionId) return;
|
|
479
|
+
|
|
480
|
+
// Fetch last assistant message (used for completion check + progress extraction)
|
|
481
|
+
const lastText = await getLastAssistantText(client, sessionId, directory, log);
|
|
482
|
+
|
|
483
|
+
if (lastText && checkCompletion(lastText)) {
|
|
484
|
+
clearState(directory, log);
|
|
485
|
+
log("info", `Loop completed at iteration ${state.iteration}`);
|
|
486
|
+
toast(`Auto Loop completed after ${state.iteration} iteration(s)`, "success");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (state.iteration >= state.maxIterations) {
|
|
491
|
+
clearState(directory, log);
|
|
492
|
+
log("warn", `Loop hit max iterations (${state.maxIterations})`);
|
|
493
|
+
toast(`Auto Loop stopped — max iterations (${state.maxIterations}) reached`, "warning");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Extract progress from last message and merge with existing state
|
|
498
|
+
const newNextSteps = lastText ? extractNextSteps(lastText) : undefined;
|
|
499
|
+
const newCompleted = lastText ? extractCompleted(lastText) : undefined;
|
|
500
|
+
|
|
501
|
+
const newState: LoopState = {
|
|
502
|
+
...state,
|
|
503
|
+
iteration: state.iteration + 1,
|
|
504
|
+
sessionId,
|
|
505
|
+
// Update next steps if we found new ones, otherwise keep previous
|
|
506
|
+
nextSteps: newNextSteps || state.nextSteps,
|
|
507
|
+
// Merge completed: append new completed items to existing
|
|
508
|
+
completed: mergeCompleted(state.completed, newCompleted),
|
|
509
|
+
};
|
|
510
|
+
writeState(directory, newState, log);
|
|
511
|
+
lastContinuation = Date.now();
|
|
512
|
+
|
|
513
|
+
// Build continuation prompt with progress context
|
|
514
|
+
const progressSection = buildProgressSection(newState);
|
|
515
|
+
|
|
516
|
+
const continuationPrompt = `[AUTO LOOP — ITERATION ${newState.iteration}/${newState.maxIterations}]
|
|
517
|
+
|
|
518
|
+
Continue working on the task. Do NOT repeat work that is already done.
|
|
519
|
+
${progressSection}
|
|
520
|
+
IMPORTANT:
|
|
521
|
+
- Pick up from the next incomplete step below
|
|
522
|
+
- When FULLY complete, output: <promise>DONE</promise>
|
|
523
|
+
- Before going idle, list your progress using ## Completed and ## Next Steps sections
|
|
524
|
+
- Do not stop until the task is truly done
|
|
525
|
+
|
|
526
|
+
Original task:
|
|
527
|
+
${state.prompt || "(no task specified)"}`;
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
await client.session.prompt({
|
|
531
|
+
path: { id: sessionId },
|
|
532
|
+
body: {
|
|
533
|
+
parts: [{ type: "text", text: continuationPrompt }],
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
log("info", `Sent continuation ${newState.iteration}/${newState.maxIterations}`);
|
|
537
|
+
toast(`Auto Loop: iteration ${newState.iteration}/${newState.maxIterations}`);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
log("error", `Failed to send continuation prompt: ${err}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// --- session.compacted: re-inject loop context after compaction ---
|
|
544
|
+
if (event.type === "session.compacted") {
|
|
545
|
+
const sessionId = event.properties.sessionID;
|
|
546
|
+
const state = readState(directory);
|
|
547
|
+
|
|
548
|
+
if (!state.active) return;
|
|
549
|
+
if (state.sessionId && state.sessionId !== sessionId) return;
|
|
550
|
+
|
|
551
|
+
// After compaction, the AI loses loop context — send a reminder
|
|
552
|
+
try {
|
|
553
|
+
await client.session.prompt({
|
|
554
|
+
path: { id: sessionId },
|
|
555
|
+
body: {
|
|
556
|
+
parts: [{ type: "text", text: buildLoopContextReminder(state) }],
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
log("info", `Re-injected loop context after compaction for session ${sessionId}`);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
log("warn", `Failed to re-inject loop context after compaction: ${err}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// --- session.error: pause the loop on error ---
|
|
566
|
+
if (event.type === "session.error") {
|
|
567
|
+
const sessionId = event.properties.sessionID;
|
|
568
|
+
const state = readState(directory);
|
|
569
|
+
|
|
570
|
+
if (
|
|
571
|
+
state.active &&
|
|
572
|
+
(!state.sessionId || state.sessionId === sessionId)
|
|
573
|
+
) {
|
|
574
|
+
log("warn", `Session error detected, pausing loop at iteration ${state.iteration}`);
|
|
575
|
+
toast("Auto Loop paused — session error", "error");
|
|
576
|
+
// Mark inactive but keep state so user can inspect/resume
|
|
577
|
+
writeState(directory, { ...state, active: false }, log);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// --- session.deleted: clean up if it's our session ---
|
|
582
|
+
if (event.type === "session.deleted") {
|
|
583
|
+
const state = readState(directory);
|
|
584
|
+
if (!state.active) return;
|
|
585
|
+
|
|
586
|
+
const deletedSessionId = event.properties.info?.id;
|
|
587
|
+
if (state.sessionId && deletedSessionId && state.sessionId !== deletedSessionId) return;
|
|
588
|
+
|
|
589
|
+
clearState(directory, log);
|
|
590
|
+
log("info", "Session deleted, cleaning up loop state");
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
export default AutoLoopPlugin;
|