@bybrawe/opencode-loop 0.5.11 → 0.5.13
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 +18 -0
- package/README.md +9 -8
- package/package.json +6 -3
- package/scripts/install-node.mjs +3 -2
- package/scripts/install.ps1 +4 -3
- package/scripts/install.sh +3 -2
- package/scripts/loopd.mjs +7 -2
- package/src/index.js +40 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.13
|
|
4
|
+
|
|
5
|
+
- Wrapped experimental goal tools with `tool()` and Zod schemas for current OpenCode tool validation.
|
|
6
|
+
- Added `@opencode-ai/plugin` as a dev dependency so fresh clones can resolve the tool helper during local development.
|
|
7
|
+
|
|
8
|
+
## 0.5.12
|
|
9
|
+
|
|
10
|
+
- Fixed local plugin installs to write `opencode-loop.ts`, matching OpenCode local plugin discovery.
|
|
11
|
+
- Removed stale `opencode-loop.js` local plugin copies during install to avoid duplicate or outdated local plugin loads.
|
|
12
|
+
- Made the PowerShell source installer honor `OPENCODE_CONFIG_DIR`, matching the npm and shell installers.
|
|
13
|
+
- Fixed `opencode-loopd` to pass the prompt to `opencode run` as the positional message instead of the non-existent `--prompt` flag.
|
|
14
|
+
- Added a short retry backoff for failing `opencode-loopd --every 0s` runs.
|
|
15
|
+
- Normalized command arguments from array/object forms so `opencode run --command loop ...` can schedule jobs like TUI slash commands.
|
|
16
|
+
- Stripped whole-argument quote wrappers before parsing loop durations, fixing quoted `opencode run --command loop "1h ..."` invocations.
|
|
17
|
+
- Fixed a goal finalization reassignment so the generated local TypeScript plugin parses under Bun.
|
|
18
|
+
- Fixed the Windows Task Scheduler helper script path used by `opencode-loopd install-task`.
|
|
19
|
+
- Tried current OpenCode SDK `{ path, body }` and `{ query }` argument shapes before older fallback shapes.
|
|
20
|
+
|
|
3
21
|
## 0.5.11
|
|
4
22
|
|
|
5
23
|
- Added a watchdog loop in addition to one-shot due timers, so recurring jobs keep checking due work even if OpenCode misses an idle event or a due timeout gets effectively stuck behind a stale busy state.
|
package/README.md
CHANGED
|
@@ -178,18 +178,19 @@ npx -y @bybrawe/opencode-loop
|
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
The installer copies the plugin and slash command files into your OpenCode config directory.
|
|
181
|
+
It also removes the old local `opencode-loop.js` copy from earlier releases so only the current local `opencode-loop.ts` plugin remains.
|
|
181
182
|
|
|
182
183
|
Windows target paths:
|
|
183
184
|
|
|
184
185
|
```text
|
|
185
|
-
%USERPROFILE%\.config\opencode\plugins\opencode-loop.
|
|
186
|
+
%USERPROFILE%\.config\opencode\plugins\opencode-loop.ts
|
|
186
187
|
%USERPROFILE%\.config\opencode\commands\loop*.md
|
|
187
188
|
```
|
|
188
189
|
|
|
189
190
|
macOS / Linux target paths:
|
|
190
191
|
|
|
191
192
|
```text
|
|
192
|
-
~/.config/opencode/plugins/opencode-loop.
|
|
193
|
+
~/.config/opencode/plugins/opencode-loop.ts
|
|
193
194
|
~/.config/opencode/commands/loop*.md
|
|
194
195
|
```
|
|
195
196
|
|
|
@@ -273,7 +274,7 @@ Windows PowerShell:
|
|
|
273
274
|
```powershell
|
|
274
275
|
mkdir "$env:USERPROFILE\.config\opencode\plugins" -Force
|
|
275
276
|
mkdir "$env:USERPROFILE\.config\opencode\commands" -Force
|
|
276
|
-
copy .\src\index.js "$env:USERPROFILE\.config\opencode\plugins\opencode-loop.
|
|
277
|
+
copy .\src\index.js "$env:USERPROFILE\.config\opencode\plugins\opencode-loop.ts"
|
|
277
278
|
copy .\commands\*.md "$env:USERPROFILE\.config\opencode\commands\"
|
|
278
279
|
```
|
|
279
280
|
|
|
@@ -281,7 +282,7 @@ macOS / Linux:
|
|
|
281
282
|
|
|
282
283
|
```bash
|
|
283
284
|
mkdir -p ~/.config/opencode/plugins ~/.config/opencode/commands
|
|
284
|
-
cp ./src/index.js ~/.config/opencode/plugins/opencode-loop.
|
|
285
|
+
cp ./src/index.js ~/.config/opencode/plugins/opencode-loop.ts
|
|
285
286
|
cp ./commands/*.md ~/.config/opencode/commands/
|
|
286
287
|
```
|
|
287
288
|
|
|
@@ -291,7 +292,7 @@ Use this when you want the plugin to be available only inside one repository.
|
|
|
291
292
|
|
|
292
293
|
```bash
|
|
293
294
|
mkdir -p .opencode/plugins .opencode/commands
|
|
294
|
-
cp ./src/index.js .opencode/plugins/opencode-loop.
|
|
295
|
+
cp ./src/index.js .opencode/plugins/opencode-loop.ts
|
|
295
296
|
cp ./commands/*.md .opencode/commands/
|
|
296
297
|
```
|
|
297
298
|
|
|
@@ -300,7 +301,7 @@ On Windows PowerShell:
|
|
|
300
301
|
```powershell
|
|
301
302
|
mkdir .opencode\plugins -Force
|
|
302
303
|
mkdir .opencode\commands -Force
|
|
303
|
-
copy .\src\index.js .opencode\plugins\opencode-loop.
|
|
304
|
+
copy .\src\index.js .opencode\plugins\opencode-loop.ts
|
|
304
305
|
copy .\commands\*.md .opencode\commands\
|
|
305
306
|
```
|
|
306
307
|
|
|
@@ -316,7 +317,7 @@ After restarting OpenCode, run:
|
|
|
316
317
|
If the commands do not appear:
|
|
317
318
|
|
|
318
319
|
1. Make sure OpenCode was fully restarted.
|
|
319
|
-
2. Check that `opencode-loop.
|
|
320
|
+
2. Check that `opencode-loop.ts` exists in the OpenCode plugin directory.
|
|
320
321
|
3. Check that `loop.md`, `loop-help.md`, and the other command files exist in the OpenCode commands directory.
|
|
321
322
|
4. Run `npx -y @bybrawe/opencode-loop` again to reinstall the command files.
|
|
322
323
|
|
|
@@ -1177,4 +1178,4 @@ Improve the application in small safe steps.
|
|
|
1177
1178
|
|
|
1178
1179
|
## License
|
|
1179
1180
|
|
|
1180
|
-
MIT
|
|
1181
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "Claude Code/Codex style /loop and experimental goal mode for OpenCode: heartbeat scheduler, idle-safe loops, scheduled commands, compact scheduling, verification, checkpoints, and persistent coding goals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -54,9 +54,12 @@
|
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@opencode-ai/plugin": ">=1.4.0"
|
|
56
56
|
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@opencode-ai/plugin": "^1.4.0"
|
|
59
|
+
},
|
|
57
60
|
"bin": {
|
|
58
|
-
"opencode-loop": "
|
|
59
|
-
"opencode-loopd": "
|
|
61
|
+
"opencode-loop": "scripts/install-node.mjs",
|
|
62
|
+
"opencode-loopd": "scripts/loopd.mjs"
|
|
60
63
|
},
|
|
61
64
|
"publishConfig": {
|
|
62
65
|
"access": "public"
|
package/scripts/install-node.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { copyFile, mkdir, readdir } from "node:fs/promises"
|
|
2
|
+
import { copyFile, mkdir, readdir, rm } from "node:fs/promises"
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { fileURLToPath } from "node:url"
|
|
@@ -11,7 +11,8 @@ const commandDir = join(config, "commands")
|
|
|
11
11
|
|
|
12
12
|
await mkdir(pluginDir, { recursive: true })
|
|
13
13
|
await mkdir(commandDir, { recursive: true })
|
|
14
|
-
await copyFile(join(root, "src", "index.js"), join(pluginDir, "opencode-loop.
|
|
14
|
+
await copyFile(join(root, "src", "index.js"), join(pluginDir, "opencode-loop.ts"))
|
|
15
|
+
await rm(join(pluginDir, "opencode-loop.js"), { force: true })
|
|
15
16
|
|
|
16
17
|
for (const name of await readdir(join(root, "commands"))) {
|
|
17
18
|
if (name.endsWith(".md")) {
|
package/scripts/install.ps1
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
$ErrorActionPreference = "Stop"
|
|
2
2
|
|
|
3
|
-
$ConfigDir = Join-Path $env:USERPROFILE ".config\opencode"
|
|
3
|
+
$ConfigDir = if ($env:OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".config\opencode" }
|
|
4
4
|
$PluginDir = Join-Path $ConfigDir "plugins"
|
|
5
5
|
$CommandDir = Join-Path $ConfigDir "commands"
|
|
6
6
|
$Root = Split-Path -Parent $PSScriptRoot
|
|
@@ -8,10 +8,11 @@ $Root = Split-Path -Parent $PSScriptRoot
|
|
|
8
8
|
New-Item -ItemType Directory -Force -Path $PluginDir | Out-Null
|
|
9
9
|
New-Item -ItemType Directory -Force -Path $CommandDir | Out-Null
|
|
10
10
|
|
|
11
|
-
Copy-Item -Force (Join-Path $Root "src\index.js") (Join-Path $PluginDir "opencode-loop.
|
|
11
|
+
Copy-Item -Force (Join-Path $Root "src\index.js") (Join-Path $PluginDir "opencode-loop.ts")
|
|
12
|
+
Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $PluginDir "opencode-loop.js")
|
|
12
13
|
Copy-Item -Force (Join-Path $Root "commands\*.md") $CommandDir
|
|
13
14
|
|
|
14
15
|
Write-Host "Installed OpenCode Loop plugin." -ForegroundColor Green
|
|
15
|
-
Write-Host "Plugin: $PluginDir\opencode-loop.
|
|
16
|
+
Write-Host "Plugin: $PluginDir\opencode-loop.ts"
|
|
16
17
|
Write-Host "Commands: $CommandDir\loop*.md"
|
|
17
18
|
Write-Host "Restart OpenCode, then run: /loop-help"
|
package/scripts/install.sh
CHANGED
|
@@ -7,10 +7,11 @@ COMMAND_DIR="$CONFIG_DIR/commands"
|
|
|
7
7
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
|
8
8
|
|
|
9
9
|
mkdir -p "$PLUGIN_DIR" "$COMMAND_DIR"
|
|
10
|
-
cp "$ROOT_DIR/src/index.js" "$PLUGIN_DIR/opencode-loop.
|
|
10
|
+
cp "$ROOT_DIR/src/index.js" "$PLUGIN_DIR/opencode-loop.ts"
|
|
11
|
+
rm -f "$PLUGIN_DIR/opencode-loop.js"
|
|
11
12
|
cp "$ROOT_DIR/commands"/*.md "$COMMAND_DIR/"
|
|
12
13
|
|
|
13
14
|
echo "Installed OpenCode Loop plugin."
|
|
14
|
-
echo "Plugin: $PLUGIN_DIR/opencode-loop.
|
|
15
|
+
echo "Plugin: $PLUGIN_DIR/opencode-loop.ts"
|
|
15
16
|
echo "Commands: $COMMAND_DIR/loop*.md"
|
|
16
17
|
echo "Restart OpenCode, then run: /loop-help"
|
package/scripts/loopd.mjs
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import fs from "node:fs"
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
5
6
|
|
|
6
7
|
const args = process.argv.slice(2)
|
|
8
|
+
const FAILED_RUN_RETRY_MS = 5_000
|
|
7
9
|
|
|
8
10
|
function arg(name, fallback = null) {
|
|
9
11
|
const i = args.indexOf(name)
|
|
@@ -97,11 +99,14 @@ async function daemon() {
|
|
|
97
99
|
console.log("")
|
|
98
100
|
console.log(`[opencode-loopd] run #${count} ${new Date().toISOString()}`)
|
|
99
101
|
|
|
100
|
-
const command = `opencode run --continue
|
|
102
|
+
const command = `opencode run --continue ${JSON.stringify(prompt)}`
|
|
101
103
|
const code = await run(command, project)
|
|
102
104
|
|
|
103
105
|
if (code !== 0) {
|
|
104
106
|
console.log(`[opencode-loopd] opencode exited with code ${code}`)
|
|
107
|
+
if (delay === 0) {
|
|
108
|
+
await sleep(FAILED_RUN_RETRY_MS)
|
|
109
|
+
}
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
if (maxRuns > 0 && count >= maxRuns) {
|
|
@@ -127,7 +132,7 @@ function installTask() {
|
|
|
127
132
|
const promptFile = arg("--prompt-file")
|
|
128
133
|
const promptArg = arg("--prompt")
|
|
129
134
|
const node = process.execPath
|
|
130
|
-
const script =
|
|
135
|
+
const script = fileURLToPath(import.meta.url)
|
|
131
136
|
|
|
132
137
|
const commandParts = [
|
|
133
138
|
`"${node}"`,
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import { spawn } from "node:child_process"
|
|
4
|
+
import { tool } from "@opencode-ai/plugin/tool"
|
|
4
5
|
|
|
5
6
|
const SERVICE = "opencode-loop"
|
|
6
7
|
const STATE_DIR = ".opencode/opencode-loop"
|
|
@@ -148,7 +149,7 @@ function parseCompactEvery(value) {
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
function parseLoopArgs(raw, defaults = {}) {
|
|
151
|
-
let input = String(raw || "").trim()
|
|
152
|
+
let input = stripOuterQuotes(String(raw || "").trim())
|
|
152
153
|
let first = ""
|
|
153
154
|
let rest = input
|
|
154
155
|
let intervalMs = defaults.intervalMs ?? null
|
|
@@ -390,8 +391,8 @@ async function executeTuiCommand(client, command) {
|
|
|
390
391
|
if (!client?.tui?.executeCommand) throw new Error("client.tui.executeCommand is not available")
|
|
391
392
|
return await sdkCall(
|
|
392
393
|
client.tui.executeCommand.bind(client.tui),
|
|
393
|
-
{ command },
|
|
394
394
|
{ body: { command } },
|
|
395
|
+
{ command },
|
|
395
396
|
)
|
|
396
397
|
}
|
|
397
398
|
|
|
@@ -415,7 +416,7 @@ async function compactSession(client, sessionID) {
|
|
|
415
416
|
}
|
|
416
417
|
}
|
|
417
418
|
try {
|
|
418
|
-
await sdkCall(client.session.summarize.bind(client.session), {
|
|
419
|
+
await sdkCall(client.session.summarize.bind(client.session), { path: { id: sessionID }, body: {} }, { sessionID })
|
|
419
420
|
return true
|
|
420
421
|
} catch (error) {
|
|
421
422
|
await log(client, "warn", "session.summarize fallback failed", { error: sdkErrorMessage(error) })
|
|
@@ -428,22 +429,22 @@ async function log(client, level, message, extra) {
|
|
|
428
429
|
try {
|
|
429
430
|
await sdkCall(
|
|
430
431
|
client.app.log.bind(client.app),
|
|
431
|
-
extra === undefined ? { service: SERVICE, level, message } : { service: SERVICE, level, message, extra },
|
|
432
432
|
{ body: extra === undefined ? { service: SERVICE, level, message } : { service: SERVICE, level, message, extra } },
|
|
433
|
+
extra === undefined ? { service: SERVICE, level, message } : { service: SERVICE, level, message, extra },
|
|
433
434
|
)
|
|
434
435
|
} catch {}
|
|
435
436
|
}
|
|
436
437
|
|
|
437
438
|
async function toast(client, message, variant = "info") {
|
|
438
|
-
try { await sdkCall(client.tui.showToast.bind(client.tui), { message, variant }, {
|
|
439
|
+
try { await sdkCall(client.tui.showToast.bind(client.tui), { body: { message, variant } }, { message, variant }) } catch {}
|
|
439
440
|
}
|
|
440
441
|
|
|
441
442
|
async function say(client, sessionID, text) {
|
|
442
443
|
try {
|
|
443
444
|
await sdkCall(
|
|
444
445
|
client.session.prompt.bind(client.session),
|
|
445
|
-
{ sessionID, noReply: true, parts: [{ type: "text", text }] },
|
|
446
446
|
{ path: { id: sessionID }, body: { noReply: true, parts: [{ type: "text", text }] } },
|
|
447
|
+
{ sessionID, noReply: true, parts: [{ type: "text", text }] },
|
|
447
448
|
)
|
|
448
449
|
} catch {}
|
|
449
450
|
}
|
|
@@ -468,6 +469,18 @@ function normalizeArgsForKey(args) {
|
|
|
468
469
|
try { return JSON.stringify(args) } catch { return String(args) }
|
|
469
470
|
}
|
|
470
471
|
|
|
472
|
+
function commandArgsText(args) {
|
|
473
|
+
if (args === undefined || args === null) return ""
|
|
474
|
+
if (typeof args === "string") return args
|
|
475
|
+
if (Array.isArray(args)) return args.map(commandArgsText).join(" ")
|
|
476
|
+
if (typeof args === "object") {
|
|
477
|
+
for (const key of ["arguments", "args", "message", "text", "value"]) {
|
|
478
|
+
if (args[key] !== undefined) return commandArgsText(args[key])
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return String(args)
|
|
482
|
+
}
|
|
483
|
+
|
|
471
484
|
function rememberSession(directory, client, sessionID) {
|
|
472
485
|
if (!sessionID) return
|
|
473
486
|
knownSessions.set(sessionID, { directory, client, seenAt: now() })
|
|
@@ -809,7 +822,7 @@ function staleActiveRun(sessionID) {
|
|
|
809
822
|
|
|
810
823
|
async function readLiveSessionStatus(client, sessionID, directory) {
|
|
811
824
|
const argsList = []
|
|
812
|
-
if (directory) argsList.push({ workspace: directory }, { directory })
|
|
825
|
+
if (directory) argsList.push({ query: { directory } }, { workspace: directory }, { directory })
|
|
813
826
|
argsList.push({})
|
|
814
827
|
for (const args of argsList) {
|
|
815
828
|
try {
|
|
@@ -1140,7 +1153,7 @@ async function finalizeActiveRun(directory, client, sessionID) {
|
|
|
1140
1153
|
if (!active) return
|
|
1141
1154
|
clearActiveRun(sessionID)
|
|
1142
1155
|
const state = await readState(directory, sessionID)
|
|
1143
|
-
|
|
1156
|
+
let job = (state.jobs || []).find((candidate) => candidate.id === active.jobId)
|
|
1144
1157
|
if (!job) return
|
|
1145
1158
|
job.lastFinishedAt = now()
|
|
1146
1159
|
|
|
@@ -1209,7 +1222,7 @@ async function fireAction(directory, client, sessionID, job) {
|
|
|
1209
1222
|
await compactSession(client, sessionID)
|
|
1210
1223
|
return { startsAssistantTurn: true }
|
|
1211
1224
|
}
|
|
1212
|
-
await sdkCall(client.session.command.bind(client.session), {
|
|
1225
|
+
await sdkCall(client.session.command.bind(client.session), { path: { id: sessionID }, body: { command, arguments: argumentsText } }, { sessionID, command, arguments: argumentsText })
|
|
1213
1226
|
return { startsAssistantTurn: true }
|
|
1214
1227
|
}
|
|
1215
1228
|
if (kind === "shell") {
|
|
@@ -1219,18 +1232,18 @@ async function fireAction(directory, client, sessionID, job) {
|
|
|
1219
1232
|
await appendLoopLog(directory, "blocked", { sessionID, job: job.name || job.id, command })
|
|
1220
1233
|
return { startsAssistantTurn: false }
|
|
1221
1234
|
}
|
|
1222
|
-
fireSdk(client, "session.shell", client.session.shell.bind(client.session), {
|
|
1235
|
+
fireSdk(client, "session.shell", client.session.shell.bind(client.session), { path: { id: sessionID }, body: { command } }, { sessionID, command })
|
|
1223
1236
|
return { startsAssistantTurn: true }
|
|
1224
1237
|
}
|
|
1225
1238
|
const prompt = await buildPrompt(directory, job)
|
|
1226
1239
|
const prefix = kind === "goal"
|
|
1227
1240
|
? "EXPERIMENTAL GOAL MODE CONTINUATION. Continue pursuing the active goal. Do not explain the /loop-goal command. Use the goal tools only when progress/completion/block state is real."
|
|
1228
1241
|
: "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."
|
|
1229
|
-
fireSdk(client, "session.prompt", client.session.prompt.bind(client.session), { sessionID, parts: [{ type: "text", text: `${prefix}
|
|
1242
|
+
fireSdk(client, "session.prompt", client.session.prompt.bind(client.session), { path: { id: sessionID }, body: { parts: [{ type: "text", text: `${prefix}
|
|
1230
1243
|
|
|
1231
|
-
${prompt}` }] }, {
|
|
1244
|
+
${prompt}` }] } }, { sessionID, parts: [{ type: "text", text: `${prefix}
|
|
1232
1245
|
|
|
1233
|
-
${prompt}` }] }
|
|
1246
|
+
${prompt}` }] })
|
|
1234
1247
|
return { startsAssistantTurn: true }
|
|
1235
1248
|
}
|
|
1236
1249
|
|
|
@@ -1346,7 +1359,7 @@ async function maybeRunDueJobs(directory, client, sessionID, options = {}) {
|
|
|
1346
1359
|
return
|
|
1347
1360
|
}
|
|
1348
1361
|
let timer
|
|
1349
|
-
if (job.timeoutMs > 0) timer = setTimeout(() => { fireSdk(client, "session.abort", client.session.abort.bind(client.session), {
|
|
1362
|
+
if (job.timeoutMs > 0) timer = setTimeout(() => { fireSdk(client, "session.abort", client.session.abort.bind(client.session), { path: { id: sessionID }, body: {} }, { sessionID }); toast(client, `Loop timeout fired: ${job.name || job.id}`, "warning").catch(() => {}) }, job.timeoutMs)
|
|
1350
1363
|
activeRuns.set(sessionID, { jobId: job.id, job, startedAt: now(), timer })
|
|
1351
1364
|
await reschedule(BUSY_RETRY_MS)
|
|
1352
1365
|
} catch (error) {
|
|
@@ -1588,7 +1601,7 @@ async function exportLoop(directory, client, sessionID) {
|
|
|
1588
1601
|
async function handleCommand(directory, client, input, fallbackName, fallbackArgs, output) {
|
|
1589
1602
|
const name = commandName(input?.command ?? input?.name ?? fallbackName)
|
|
1590
1603
|
const sessionID = input?.sessionID
|
|
1591
|
-
const args = input?.arguments ?? fallbackArgs ?? ""
|
|
1604
|
+
const args = commandArgsText(input?.arguments ?? fallbackArgs ?? "")
|
|
1592
1605
|
if (!sessionID || !name) return false
|
|
1593
1606
|
rememberSession(directory, client, sessionID)
|
|
1594
1607
|
if (wasHandled(sessionID, name, args)) return true
|
|
@@ -1628,39 +1641,39 @@ async function handleCommand(directory, client, input, fallbackName, fallbackArg
|
|
|
1628
1641
|
|
|
1629
1642
|
function goalTools(defaultDirectory) {
|
|
1630
1643
|
return {
|
|
1631
|
-
opencode_loop_goal_complete: {
|
|
1644
|
+
opencode_loop_goal_complete: tool({
|
|
1632
1645
|
description: "Mark the current OpenCode Loop experimental goal as completed. Use only after acceptance criteria are satisfied and you have evidence from tests, typecheck, build, or code inspection.",
|
|
1633
1646
|
args: {
|
|
1634
|
-
summary:
|
|
1635
|
-
evidence:
|
|
1647
|
+
summary: tool.schema.string().describe("Short human-readable summary of what was completed."),
|
|
1648
|
+
evidence: tool.schema.string().describe("Concrete evidence that the goal is complete, such as commands run, passing checks, files changed, and important results."),
|
|
1636
1649
|
},
|
|
1637
1650
|
execute: async (args, context) => {
|
|
1638
1651
|
const result = await setGoalComplete(context.directory || defaultDirectory, context.sessionID, args)
|
|
1639
1652
|
return { title: result.ok ? "Goal completed" : "Goal not found", output: result.message }
|
|
1640
1653
|
},
|
|
1641
|
-
},
|
|
1642
|
-
opencode_loop_goal_blocked: {
|
|
1654
|
+
}),
|
|
1655
|
+
opencode_loop_goal_blocked: tool({
|
|
1643
1656
|
description: "Mark the current OpenCode Loop experimental goal as blocked when user input or manual intervention is required.",
|
|
1644
1657
|
args: {
|
|
1645
|
-
reason:
|
|
1646
|
-
needed:
|
|
1658
|
+
reason: tool.schema.string().describe("Why the goal is blocked."),
|
|
1659
|
+
needed: tool.schema.string().describe("What user input, credential, decision, or manual action is needed to continue."),
|
|
1647
1660
|
},
|
|
1648
1661
|
execute: async (args, context) => {
|
|
1649
1662
|
const result = await setGoalBlocked(context.directory || defaultDirectory, context.sessionID, args)
|
|
1650
1663
|
return { title: result.ok ? "Goal blocked" : "Goal not found", output: result.message }
|
|
1651
1664
|
},
|
|
1652
|
-
},
|
|
1653
|
-
opencode_loop_goal_progress: {
|
|
1665
|
+
}),
|
|
1666
|
+
opencode_loop_goal_progress: tool({
|
|
1654
1667
|
description: "Record meaningful progress on the current OpenCode Loop experimental goal without completing it.",
|
|
1655
1668
|
args: {
|
|
1656
|
-
summary:
|
|
1657
|
-
next:
|
|
1669
|
+
summary: tool.schema.string().describe("What useful progress was made."),
|
|
1670
|
+
next: tool.schema.string().describe("The next step toward completing the goal."),
|
|
1658
1671
|
},
|
|
1659
1672
|
execute: async (args, context) => {
|
|
1660
1673
|
const result = await setGoalProgress(context.directory || defaultDirectory, context.sessionID, args)
|
|
1661
1674
|
return { title: result.ok ? "Goal progress" : "Goal not found", output: result.message }
|
|
1662
1675
|
},
|
|
1663
|
-
},
|
|
1676
|
+
}),
|
|
1664
1677
|
}
|
|
1665
1678
|
}
|
|
1666
1679
|
|