@bybrawe/opencode-loop 0.4.2 → 0.5.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/CHANGELOG.md +25 -0
- package/README.md +784 -672
- package/commands/loop-clear.md +2 -2
- package/commands/loop-compact.md +2 -2
- package/commands/loop-dev.md +2 -2
- package/commands/loop-doctor.md +2 -2
- package/commands/loop-export.md +2 -2
- package/commands/loop-help.md +2 -2
- package/commands/loop-init.md +2 -2
- package/commands/loop-logs.md +2 -2
- package/commands/loop-now.md +2 -2
- package/commands/loop-pause.md +2 -2
- package/commands/loop-progress.md +2 -2
- package/commands/loop-remove.md +2 -2
- package/commands/loop-resume.md +2 -2
- package/commands/loop-safe-dev.md +2 -2
- package/commands/loop-status.md +2 -2
- package/commands/loop-stop.md +2 -2
- package/commands/loop-testfix.md +2 -2
- package/commands/loop.md +2 -2
- package/examples/loop-prompt.md +10 -0
- package/package.json +9 -5
- package/scripts/loopd.mjs +205 -0
- package/src/index.js +37 -3
package/commands/loop-clear.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-compact.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-dev.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-doctor.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-export.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-help.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Show OpenCode Loop help
|
|
2
|
+
description: Show OpenCode Loop help.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-init.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-logs.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Show
|
|
2
|
+
description: Show OpenCode Loop logs.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-now.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-pause.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-remove.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-resume.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-status.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Show
|
|
2
|
+
description: Show OpenCode Loop status.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-stop.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Stop
|
|
2
|
+
description: Stop OpenCode Loop jobs.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop-testfix.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: OpenCode Loop plugin command.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/commands/loop.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Start an OpenCode
|
|
2
|
+
description: Start an OpenCode auto-continue loop. Usage: /loop 5m <task>
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Loop command received. The OpenCode Loop plugin handles this command. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Continue from progress.md and implement the next unfinished TODO.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- Do not ask questions.
|
|
5
|
+
- Make reasonable assumptions.
|
|
6
|
+
- Mark completed TODO items with [x].
|
|
7
|
+
- Add useful follow-up TODOs when needed.
|
|
8
|
+
- Run tests/lint/build when available.
|
|
9
|
+
- Do not run destructive commands such as git reset, git clean, rm -rf, force push, deploy, or production migrations.
|
|
10
|
+
- Keep going while work remains.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Claude Code style
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Claude Code style /loop command and background daemon for OpenCode: auto-continue, progress.md TODO automation, compact scheduling, verification, checkpoints, and safe autonomous coding loops.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"CHANGELOG.md"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"check": "node --check src/index.js",
|
|
20
|
+
"check": "node --check src/index.js && node --check scripts/install-node.mjs && node --check scripts/loopd.mjs",
|
|
21
21
|
"install:global": "node scripts/install-node.mjs",
|
|
22
22
|
"pack:zip": "node scripts/make-zip.mjs"
|
|
23
23
|
},
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
"scheduler",
|
|
37
37
|
"progress-md",
|
|
38
38
|
"todo-automation",
|
|
39
|
-
"ralph-loop"
|
|
39
|
+
"ralph-loop",
|
|
40
|
+
"daemon",
|
|
41
|
+
"background-runner",
|
|
42
|
+
"windows-task-scheduler"
|
|
40
43
|
],
|
|
41
44
|
"author": "ByBrawe",
|
|
42
45
|
"license": "MIT",
|
|
@@ -52,6 +55,7 @@
|
|
|
52
55
|
"@opencode-ai/plugin": ">=1.4.0"
|
|
53
56
|
},
|
|
54
57
|
"bin": {
|
|
55
|
-
"opencode-loop": "./scripts/install-node.mjs"
|
|
58
|
+
"opencode-loop": "./scripts/install-node.mjs",
|
|
59
|
+
"opencode-loopd": "./scripts/loopd.mjs"
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2)
|
|
7
|
+
|
|
8
|
+
function arg(name, fallback = null) {
|
|
9
|
+
const i = args.indexOf(name)
|
|
10
|
+
return i >= 0 ? args[i + 1] ?? fallback : fallback
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function has(name) {
|
|
14
|
+
return args.includes(name)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseMs(value) {
|
|
18
|
+
const v = String(value || "0s").trim().toLowerCase()
|
|
19
|
+
if (v === "0" || v === "0s" || v === "now") return 0
|
|
20
|
+
|
|
21
|
+
const m = v.match(/^(\d+(?:\.\d+)?)\s*(ms|s|sec|secs|m|min|mins|h|hr|hrs|d|day|days)$/)
|
|
22
|
+
if (!m) throw new Error(`Invalid duration: ${value}`)
|
|
23
|
+
|
|
24
|
+
const n = Number(m[1])
|
|
25
|
+
const unit = m[2]
|
|
26
|
+
|
|
27
|
+
if (unit === "ms") return n
|
|
28
|
+
if (unit.startsWith("s")) return n * 1000
|
|
29
|
+
if (unit.startsWith("m")) return n * 60_000
|
|
30
|
+
if (unit.startsWith("h")) return n * 3_600_000
|
|
31
|
+
if (unit.startsWith("d")) return n * 86_400_000
|
|
32
|
+
|
|
33
|
+
return n
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sleep(ms) {
|
|
37
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(command, cwd) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const child = spawn(command, {
|
|
43
|
+
cwd,
|
|
44
|
+
shell: true,
|
|
45
|
+
stdio: "inherit",
|
|
46
|
+
env: process.env,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
child.on("exit", (code) => resolve(code ?? 0))
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readPrompt(project) {
|
|
54
|
+
const promptFile = arg("--prompt-file")
|
|
55
|
+
const promptArg = arg("--prompt")
|
|
56
|
+
|
|
57
|
+
if (promptFile) {
|
|
58
|
+
return fs.readFileSync(path.resolve(project, promptFile), "utf8")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (promptArg) return promptArg
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
"Continue from progress.md and implement the next unfinished TODO.",
|
|
65
|
+
"Do not ask questions.",
|
|
66
|
+
"Make reasonable assumptions.",
|
|
67
|
+
"Mark completed TODO items with [x].",
|
|
68
|
+
"Add useful follow-up TODOs when needed.",
|
|
69
|
+
"Run tests/lint/build when available.",
|
|
70
|
+
"Do not run destructive commands such as git reset, git clean, rm -rf, force push, deploy, or production migrations.",
|
|
71
|
+
"Keep going while work remains.",
|
|
72
|
+
].join(" ")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function daemon() {
|
|
76
|
+
const project = path.resolve(arg("--project", process.cwd()))
|
|
77
|
+
const every = arg("--every", "0s")
|
|
78
|
+
const delay = parseMs(every)
|
|
79
|
+
const maxRuns = Number(arg("--max-runs", "0")) || 0
|
|
80
|
+
const sleepFirst = has("--sleep-first")
|
|
81
|
+
const prompt = readPrompt(project)
|
|
82
|
+
|
|
83
|
+
console.log("OpenCode Loop daemon")
|
|
84
|
+
console.log(`project: ${project}`)
|
|
85
|
+
console.log(`every: ${every}`)
|
|
86
|
+
console.log(`maxRuns: ${maxRuns || "unlimited"}`)
|
|
87
|
+
|
|
88
|
+
let count = 0
|
|
89
|
+
|
|
90
|
+
if (sleepFirst && delay > 0) {
|
|
91
|
+
await sleep(delay)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
while (true) {
|
|
95
|
+
count += 1
|
|
96
|
+
|
|
97
|
+
console.log("")
|
|
98
|
+
console.log(`[opencode-loopd] run #${count} ${new Date().toISOString()}`)
|
|
99
|
+
|
|
100
|
+
const command = `opencode run --continue --prompt ${JSON.stringify(prompt)}`
|
|
101
|
+
const code = await run(command, project)
|
|
102
|
+
|
|
103
|
+
if (code !== 0) {
|
|
104
|
+
console.log(`[opencode-loopd] opencode exited with code ${code}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (maxRuns > 0 && count >= maxRuns) {
|
|
108
|
+
console.log("[opencode-loopd] max runs reached")
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (delay > 0) {
|
|
113
|
+
await sleep(delay)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function installTask() {
|
|
119
|
+
if (process.platform !== "win32") {
|
|
120
|
+
throw new Error("install-task is currently implemented for Windows Task Scheduler only. Use daemon mode on macOS/Linux.")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const project = path.resolve(arg("--project", process.cwd()))
|
|
124
|
+
const every = arg("--every", "10m")
|
|
125
|
+
const minutes = Math.max(1, Math.round(parseMs(every) / 60_000))
|
|
126
|
+
const name = arg("--name", "OpenCodeLoop")
|
|
127
|
+
const promptFile = arg("--prompt-file")
|
|
128
|
+
const promptArg = arg("--prompt")
|
|
129
|
+
const node = process.execPath
|
|
130
|
+
const script = path.resolve(new URL(import.meta.url).pathname)
|
|
131
|
+
|
|
132
|
+
const commandParts = [
|
|
133
|
+
`"${node}"`,
|
|
134
|
+
`"${script}"`,
|
|
135
|
+
"daemon",
|
|
136
|
+
"--project",
|
|
137
|
+
`"${project}"`,
|
|
138
|
+
"--every",
|
|
139
|
+
"0s",
|
|
140
|
+
"--max-runs",
|
|
141
|
+
"1",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
if (promptFile) commandParts.push("--prompt-file", `"${promptFile}"`)
|
|
145
|
+
if (promptArg) commandParts.push("--prompt", `"${promptArg.replaceAll('"', '\\"')}"`)
|
|
146
|
+
|
|
147
|
+
const taskCommand = commandParts.join(" ")
|
|
148
|
+
const schtasks = `schtasks /Create /F /SC MINUTE /MO ${minutes} /TN "${name}" /TR ${JSON.stringify(taskCommand)}`
|
|
149
|
+
|
|
150
|
+
console.log(schtasks)
|
|
151
|
+
const result = spawnSync(schtasks, { shell: true, stdio: "inherit" })
|
|
152
|
+
process.exit(result.status ?? 0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function uninstallTask() {
|
|
156
|
+
if (process.platform !== "win32") {
|
|
157
|
+
throw new Error("uninstall-task is currently implemented for Windows Task Scheduler only.")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const name = arg("--name", "OpenCodeLoop")
|
|
161
|
+
const result = spawnSync(`schtasks /Delete /F /TN "${name}"`, { shell: true, stdio: "inherit" })
|
|
162
|
+
process.exit(result.status ?? 0)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function help() {
|
|
166
|
+
console.log(`
|
|
167
|
+
OpenCode Loop daemon
|
|
168
|
+
|
|
169
|
+
Usage:
|
|
170
|
+
opencode-loopd --project . --every 5m --prompt-file loop-prompt.md
|
|
171
|
+
opencode-loopd --project . --every 0s --prompt "continue from progress.md"
|
|
172
|
+
opencode-loopd install-task --project . --every 10m --prompt-file loop-prompt.md --name OpenCodeLoop
|
|
173
|
+
opencode-loopd uninstall-task --name OpenCodeLoop
|
|
174
|
+
|
|
175
|
+
Options:
|
|
176
|
+
--project <path> Project directory
|
|
177
|
+
--every <duration> 0s, 5m, 1h, etc.
|
|
178
|
+
--prompt <text> Prompt text
|
|
179
|
+
--prompt-file <file> Read prompt from file relative to the project
|
|
180
|
+
--max-runs <n> Stop after n runs
|
|
181
|
+
--sleep-first Wait before first run
|
|
182
|
+
`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const command = args[0]
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
if (command === "daemon" || command === "loopd") {
|
|
189
|
+
args.shift()
|
|
190
|
+
await daemon()
|
|
191
|
+
} else if (command === "install-task") {
|
|
192
|
+
args.shift()
|
|
193
|
+
installTask()
|
|
194
|
+
} else if (command === "uninstall-task") {
|
|
195
|
+
args.shift()
|
|
196
|
+
uninstallTask()
|
|
197
|
+
} else if (has("--help") || has("-h") || command === "help") {
|
|
198
|
+
help()
|
|
199
|
+
} else {
|
|
200
|
+
await daemon()
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error(error?.message || error)
|
|
204
|
+
process.exit(1)
|
|
205
|
+
}
|
package/src/index.js
CHANGED
|
@@ -186,6 +186,7 @@ function parseLoopArgs(raw, defaults = {}) {
|
|
|
186
186
|
gitCheckpoint: defaults.gitCheckpoint ?? false,
|
|
187
187
|
checkpointOnly: defaults.checkpointOnly ?? false,
|
|
188
188
|
dryRun: defaults.dryRun ?? false,
|
|
189
|
+
multi: defaults.multi ?? false,
|
|
189
190
|
batch: defaults.batch ?? 0,
|
|
190
191
|
runCount: 0,
|
|
191
192
|
failureCount: 0,
|
|
@@ -212,6 +213,8 @@ function parseLoopArgs(raw, defaults = {}) {
|
|
|
212
213
|
;[found, rest] = takeFlag(rest, "--checkpoint-only"); if (found) job.checkpointOnly = true
|
|
213
214
|
;[found, rest] = takeFlag(rest, "--pause-on-verify-fail"); if (found) job.pauseOnVerifyFail = true
|
|
214
215
|
;[found, rest] = takeFlag(rest, "--dry-run"); if (found) job.dryRun = true
|
|
216
|
+
;[found, rest] = takeFlag(rest, "--multi"); if (found) job.multi = true
|
|
217
|
+
;[found, rest] = takeFlag(rest, "--replace"); if (found) job.multi = false
|
|
215
218
|
|
|
216
219
|
;[value, rest] = takeFlagValue(rest, "--name"); if (value !== undefined) job.name = value.trim()
|
|
217
220
|
;[value, rest] = takeFlagValue(rest, "--max-runs"); if (value !== undefined) job.maxRuns = parsePositiveInt(value, 0)
|
|
@@ -606,7 +609,7 @@ async function fireAction(directory, client, sessionID, job) {
|
|
|
606
609
|
return { startsAssistantTurn: true }
|
|
607
610
|
}
|
|
608
611
|
const prompt = await buildPrompt(directory, job)
|
|
609
|
-
client.session.prompt({ path: { id: sessionID }, body: { parts: [{ type: "text", text: `
|
|
612
|
+
client.session.prompt({ path: { id: sessionID }, body: { parts: [{ type: "text", text: `AUTONOMOUS OPENCODE LOOP ITERATION. Continue the configured task now. Do not explain the /loop command. Do not search for documentation about this plugin. Do not create scheduler files. Do not ask questions. Make reasonable assumptions and work directly.\n\n${prompt}` }] } }).catch(() => {})
|
|
610
613
|
return { startsAssistantTurn: true }
|
|
611
614
|
}
|
|
612
615
|
|
|
@@ -699,16 +702,47 @@ async function maybeRunDueJobs(directory, client, sessionID, options = {}) {
|
|
|
699
702
|
}
|
|
700
703
|
}
|
|
701
704
|
|
|
705
|
+
function normalizeActionForCompare(value) {
|
|
706
|
+
return String(value || "").replace(/\s+/g, " ").trim()
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function sameLoopDefinition(a, b) {
|
|
710
|
+
if (!a || !b) return false
|
|
711
|
+
return (a.name || "") === (b.name || "") &&
|
|
712
|
+
Number(a.intervalMs || 0) === Number(b.intervalMs || 0) &&
|
|
713
|
+
normalizeActionForCompare(a.action) === normalizeActionForCompare(b.action) &&
|
|
714
|
+
normalizeActionForCompare(a.promptFile) === normalizeActionForCompare(b.promptFile)
|
|
715
|
+
}
|
|
716
|
+
|
|
702
717
|
async function addLoop(directory, client, sessionID, args, defaults = {}) {
|
|
703
718
|
const parsed = parseLoopArgs(args, defaults)
|
|
704
719
|
if (!parsed.ok) { await toast(client, parsed.error, "warning"); return }
|
|
705
720
|
if (parsed.job.watchPaths.length) parsed.job.watchSnapshot = await snapshotPaths(directory, parsed.job.watchPaths)
|
|
706
721
|
if (parsed.job.dryRun) { await toast(client, `Loop dry run: ${jobLabel(parsed.job)}`, "info"); await say(client, sessionID, "OpenCode loop dry run:\n```json\n" + JSON.stringify(parsed.job, null, 2) + "\n```"); return }
|
|
707
722
|
const state = await readState(directory, sessionID)
|
|
723
|
+
const jobs = Array.isArray(state.jobs) ? state.jobs : []
|
|
724
|
+
|
|
725
|
+
// Default behavior is replace/upsert, not append forever. This prevents duplicate
|
|
726
|
+
// loops when OpenCode emits both command.execute.before and command.executed,
|
|
727
|
+
// and it matches the common expectation that /loop configures the current loop.
|
|
728
|
+
let replaced = false
|
|
729
|
+
if (!parsed.job.multi) {
|
|
730
|
+
const targetName = parsed.job.name || "default"
|
|
731
|
+
parsed.job.name = targetName
|
|
732
|
+
state.jobs = jobs.filter((existing) => {
|
|
733
|
+
const existingName = existing.name || "default"
|
|
734
|
+
const shouldReplace = existingName === targetName || sameLoopDefinition(existing, parsed.job)
|
|
735
|
+
if (shouldReplace) replaced = true
|
|
736
|
+
return !shouldReplace
|
|
737
|
+
})
|
|
738
|
+
} else {
|
|
739
|
+
state.jobs = jobs
|
|
740
|
+
}
|
|
741
|
+
|
|
708
742
|
state.jobs.push(parsed.job)
|
|
709
743
|
await writeState(directory, sessionID, state)
|
|
710
|
-
await toast(client,
|
|
711
|
-
await appendLoopLog(directory, "add", { sessionID, job: parsed.job.name || parsed.job.id, label: jobLabel(parsed.job) })
|
|
744
|
+
await toast(client, `${replaced ? "Loop replaced" : "Loop added"}: ${jobLabel(parsed.job)}`, "success")
|
|
745
|
+
await appendLoopLog(directory, replaced ? "replace" : "add", { sessionID, job: parsed.job.name || parsed.job.id, label: jobLabel(parsed.job) })
|
|
712
746
|
}
|
|
713
747
|
|
|
714
748
|
async function stopLoop(directory, client, sessionID, args) {
|