@bybrawe/opencode-loop 0.5.31 → 0.5.32
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 +7 -0
- package/README.md +28 -8
- package/agents/opencode-loop-local.md +2 -1
- package/package.json +1 -1
- package/scripts/goal-companion-test.mjs +37 -1
- package/scripts/install-with-goals.mjs +26 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.32
|
|
4
|
+
|
|
5
|
+
- Hide the tool-denied `opencode-loop-local` acknowledgement agent from the normal primary-agent cycle by installing it as a hidden subagent while keeping Loop slash commands explicitly bound to it.
|
|
6
|
+
- Add `--without-loop-goals` so Loop installs/updates can omit the packaged experimental `/loop-goal*` command files without removing normal Loop commands, Loop runtime state, or the separate OpenCode Goals plugin.
|
|
7
|
+
- Keep backward compatibility by installing the older Loop Goal command surface by default; the new opt-out composes with `--with-goals` for a cleaner `/loop` + dedicated `/goal` setup.
|
|
8
|
+
- Add deterministic installer coverage for hidden-agent metadata, default legacy-command compatibility, clean command-surface opt-out, and combined Loop + Goals installation.
|
|
9
|
+
|
|
3
10
|
## 0.5.31
|
|
4
11
|
|
|
5
12
|
- Add best-effort experimental OpenCode 2 runtime logging in the existing JSON-lines `loop.log` format, tagged with `v2: true`, plus experimental `/loop-logs` diagnostics for recent V2 records. Logging remains non-critical and does not change the stable OpenCode 1.x support claim.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
OpenCode Loop adds `/loop`, scheduled prompt/command/shell jobs, compact scheduling, safe long-running continuation helpers, and the `opencode-loopd` background daemon.
|
|
6
6
|
|
|
7
|
-
> **Current release: `0.5.
|
|
7
|
+
> **Current release: `0.5.32`.** Loop also contains an older experimental `/loop-goal` mode, but for strong persistent Goal contracts and host-verified completion, use the separate **OpenCode Goals** plugin described below.
|
|
8
8
|
|
|
9
9
|
## Install or update
|
|
10
10
|
|
|
@@ -24,6 +24,14 @@ Install Loop and Goals together from scratch with:
|
|
|
24
24
|
npx -y @bybrawe/opencode-loop@latest --with-goals
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
If you use the dedicated OpenCode Goals plugin and do not want Loop's older experimental `/loop-goal*` commands in the slash-command list, use:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx -y @bybrawe/opencode-loop@latest --with-goals --without-loop-goals
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`--without-loop-goals` removes only Loop's packaged experimental `/loop-goal*` command files for that install/update. It keeps `/loop`, `/loop-command`, `/loop-shell`, the Loop runtime, and the separate OpenCode Goals `/goal` workflow. The choice is not persisted: run the flag again on a later Loop update if you want to keep those legacy command files omitted.
|
|
34
|
+
|
|
27
35
|
Update/install only Loop and skip all Goals companion network work with:
|
|
28
36
|
|
|
29
37
|
```bash
|
|
@@ -54,17 +62,23 @@ If you want the global Loop installer to install Goals too, run:
|
|
|
54
62
|
opencode-loop --with-goals
|
|
55
63
|
```
|
|
56
64
|
|
|
65
|
+
For the cleaner command surface with the dedicated Goals plugin:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
opencode-loop --with-goals --without-loop-goals
|
|
69
|
+
```
|
|
70
|
+
|
|
57
71
|
`npm install @bybrawe/opencode-loop` by itself only adds the Node package to the current project. For a normal OpenCode installation, use the global npm method above or the recommended `npx` installer.
|
|
58
72
|
|
|
59
73
|
The installer:
|
|
60
74
|
|
|
61
75
|
- installs/updates the OpenCode Loop plugin;
|
|
62
76
|
- installs the `/loop-*` slash-command definitions;
|
|
63
|
-
- installs the tool-denied `opencode-loop-local` command
|
|
77
|
+
- installs the tool-denied, hidden `opencode-loop-local` acknowledgement subagent used only by Loop's bound local command definitions;
|
|
64
78
|
- keeps an existing npm plugin entry pinned to the installer’s exact version to avoid stale OpenCode package-cache resolution;
|
|
65
79
|
- removes duplicate old local Loop plugin copies when the package entry is authoritative;
|
|
66
80
|
- refreshes OpenCode Goals through **Goals' own official installer** when an existing managed Goals installation is detected;
|
|
67
|
-
- can explicitly install both packages with `--with-goals`,
|
|
81
|
+
- can explicitly install both packages with `--with-goals`, skip companion network work with `--loop-only`, or omit the packaged experimental `/loop-goal*` command files with `--without-loop-goals`.
|
|
68
82
|
|
|
69
83
|
Then **fully restart OpenCode** and verify:
|
|
70
84
|
|
|
@@ -166,7 +180,7 @@ Loop is idle-safe: if a job becomes due while the session is busy, active tools
|
|
|
166
180
|
|
|
167
181
|
## For stronger persistent Goals, use OpenCode Goals
|
|
168
182
|
|
|
169
|
-
OpenCode Loop still includes the older **experimental** `/loop-goal` workflow
|
|
183
|
+
OpenCode Loop still includes the older **experimental** `/loop-goal` workflow for backward compatibility unless you install/update with `--without-loop-goals`. It is useful for compatibility and lightweight outcome-driven automation, but it is not the strongest Goal implementation in this project family.
|
|
170
184
|
|
|
171
185
|
For durable Goal Contracts, host-owned evidence, semantic verification, native Todo coordination, revision isolation, false-completion protection, restart recovery, Goal audit, budgets, and ordered Goals, install **OpenCode Goals**.
|
|
172
186
|
|
|
@@ -176,6 +190,12 @@ Convenient combined installer from Loop:
|
|
|
176
190
|
npx -y @bybrawe/opencode-loop@latest --with-goals
|
|
177
191
|
```
|
|
178
192
|
|
|
193
|
+
For new installations where the dedicated `/goal` workflow replaces Loop's older experimental Goal commands, prefer:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npx -y @bybrawe/opencode-loop@latest --with-goals --without-loop-goals
|
|
197
|
+
```
|
|
198
|
+
|
|
179
199
|
Or install Goals directly with its standalone installer:
|
|
180
200
|
|
|
181
201
|
```bash
|
|
@@ -207,7 +227,7 @@ Recommended split:
|
|
|
207
227
|
- use **`/goal`** for persistent, strongly verified outcome completion;
|
|
208
228
|
- use **`/loop`**, `/loop-command`, `/loop-shell`, and `opencode-loopd` for scheduling/repetition/background infrastructure.
|
|
209
229
|
|
|
210
|
-
Do **not** run Loop’s `/loop-goal` and OpenCode Goals `/goal` against the same work in the same session. Both can autonomously continue on idle boundaries and can compete to start turns.
|
|
230
|
+
Do **not** run Loop’s `/loop-goal` and OpenCode Goals `/goal` against the same work in the same session. Both can autonomously continue on idle boundaries and can compete to start turns. Use `--without-loop-goals` when you want to remove that legacy command surface entirely.
|
|
211
231
|
|
|
212
232
|
Likewise, avoid leaving a prompt-producing `/loop ...` job continuously injecting agent turns into a session while an OpenCode Goal is actively continuing. Use separate sessions or pause/remove that prompt loop until the Goal is done. Scheduled shell/command jobs should also be chosen carefully so they do not race files or verification.
|
|
213
233
|
|
|
@@ -224,7 +244,7 @@ npm install -g @bybrawe/opencode-loop@latest @bybrawe/opencode-goal@latest
|
|
|
224
244
|
opencode-loop --with-goals
|
|
225
245
|
```
|
|
226
246
|
|
|
227
|
-
Once Goals is managed in the OpenCode config, future normal `opencode-loop` / `npx ...opencode-loop@latest` updates also attempt to refresh Goals to its latest stable release. Use `--loop-only` when you intentionally do not want that companion refresh.
|
|
247
|
+
Once Goals is managed in the OpenCode config, future normal `opencode-loop` / `npx ...opencode-loop@latest` updates also attempt to refresh Goals to its latest stable release. Use `--loop-only` when you intentionally do not want that companion refresh. Use `--without-loop-goals` on any install/update where you want the older Loop Goal command files omitted.
|
|
228
248
|
|
|
229
249
|
## Core commands
|
|
230
250
|
|
|
@@ -244,7 +264,7 @@ Once Goals is managed in the OpenCode config, future normal `opencode-loop` / `n
|
|
|
244
264
|
| `/loop-doctor` | Diagnose Loop/OpenCode state |
|
|
245
265
|
| `/loop-init` | Create a starter `progress.md` |
|
|
246
266
|
| `/loop-export` | Export Loop state as JSON |
|
|
247
|
-
| `/loop-goal <objective>` | Start the older experimental Loop Goal Mode |
|
|
267
|
+
| `/loop-goal <objective>` | Start the older experimental Loop Goal Mode when those optional legacy command files are installed |
|
|
248
268
|
|
|
249
269
|
## Intervals and job types
|
|
250
270
|
|
|
@@ -386,7 +406,7 @@ opencode-loopd uninstall-task --name OpenCodeLoop
|
|
|
386
406
|
|
|
387
407
|
## Experimental Loop Goal Mode
|
|
388
408
|
|
|
389
|
-
Loop’s original Goal Mode remains available for existing users
|
|
409
|
+
Loop’s original Goal Mode remains available for existing users by default. If you install/update Loop with `--without-loop-goals`, its packaged `/loop-goal*` command definitions are removed while the rest of Loop keeps working.
|
|
390
410
|
|
|
391
411
|
```text
|
|
392
412
|
/loop-goal --check "npm test" --complete-when-checks-pass fix the failing tests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
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",
|
|
@@ -62,6 +62,20 @@ async function calls(log) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
async function loopGoalCommands(config) {
|
|
66
|
+
try {
|
|
67
|
+
return (await fs.readdir(path.join(config, "commands")))
|
|
68
|
+
.filter((name) => name === "loop-goal.md" || (name.startsWith("loop-goal-") && name.endsWith(".md")))
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error?.code === "ENOENT") return []
|
|
71
|
+
throw error
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function exists(target) {
|
|
76
|
+
try { await fs.access(target); return true } catch { return false }
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
async function writeConfig(config, plugins, comment = "") {
|
|
66
80
|
await fs.mkdir(config, { recursive: true })
|
|
67
81
|
await fs.writeFile(
|
|
@@ -71,12 +85,26 @@ async function writeConfig(config, plugins, comment = "") {
|
|
|
71
85
|
)
|
|
72
86
|
}
|
|
73
87
|
|
|
88
|
+
const localAgent = await fs.readFile(path.join(root, "agents", "opencode-loop-local.md"), "utf8")
|
|
89
|
+
assert.match(localAgent, /^mode:\s*subagent$/m, "the local acknowledgement agent must not participate in the primary-agent cycle")
|
|
90
|
+
assert.match(localAgent, /^hidden:\s*true$/m, "the local acknowledgement agent must be hidden from normal subagent selection")
|
|
91
|
+
|
|
74
92
|
try {
|
|
75
93
|
const absentConfig = path.join(temporaryRoot, "absent")
|
|
76
94
|
const absentLog = path.join(temporaryRoot, "absent.log")
|
|
77
95
|
const absent = await runInstaller(absentConfig, [], { FAKE_NPM_LOG: absentLog })
|
|
78
96
|
assert.equal(absent.code, 0, absent.stderr)
|
|
79
97
|
assert.deepEqual(await calls(absentLog), [], "Loop-only installations must not silently add Goals")
|
|
98
|
+
assert.ok((await loopGoalCommands(absentConfig)).length > 0, "legacy Loop Goal commands stay installed by default for compatibility")
|
|
99
|
+
|
|
100
|
+
const withoutLoopGoalsConfig = path.join(temporaryRoot, "without-loop-goals")
|
|
101
|
+
const withoutLoopGoalsLog = path.join(temporaryRoot, "without-loop-goals.log")
|
|
102
|
+
const withoutLoopGoals = await runInstaller(withoutLoopGoalsConfig, ["--without-loop-goals"], { FAKE_NPM_LOG: withoutLoopGoalsLog })
|
|
103
|
+
assert.equal(withoutLoopGoals.code, 0, withoutLoopGoals.stderr)
|
|
104
|
+
assert.deepEqual(await calls(withoutLoopGoalsLog), [], "omitting legacy Loop Goal commands must not silently install Goals")
|
|
105
|
+
assert.deepEqual(await loopGoalCommands(withoutLoopGoalsConfig), [], "--without-loop-goals must remove every packaged experimental /loop-goal* command")
|
|
106
|
+
assert.equal(await exists(path.join(withoutLoopGoalsConfig, "commands", "loop.md")), true, "normal Loop commands must remain installed")
|
|
107
|
+
assert.match(withoutLoopGoals.stdout, /Omitted \d+ packaged experimental \/loop-goal\*/)
|
|
80
108
|
|
|
81
109
|
const existingConfig = path.join(temporaryRoot, "existing")
|
|
82
110
|
const existingLog = path.join(temporaryRoot, "existing.log")
|
|
@@ -114,6 +142,13 @@ try {
|
|
|
114
142
|
assert.equal(explicit.code, 0, explicit.stderr)
|
|
115
143
|
assert.deepEqual(await calls(explicitLog), [expectedGoalExec], "--with-goals must install the companion even when it is absent")
|
|
116
144
|
|
|
145
|
+
const explicitCleanConfig = path.join(temporaryRoot, "explicit-clean")
|
|
146
|
+
const explicitCleanLog = path.join(temporaryRoot, "explicit-clean.log")
|
|
147
|
+
const explicitClean = await runInstaller(explicitCleanConfig, ["--with-goals", "--without-loop-goals"], { FAKE_NPM_LOG: explicitCleanLog })
|
|
148
|
+
assert.equal(explicitClean.code, 0, explicitClean.stderr)
|
|
149
|
+
assert.deepEqual(await calls(explicitCleanLog), [expectedGoalExec], "the clean command surface option must compose with --with-goals")
|
|
150
|
+
assert.deepEqual(await loopGoalCommands(explicitCleanConfig), [], "combined install must keep the stronger Goals plugin without legacy Loop Goal command clutter")
|
|
151
|
+
|
|
117
152
|
const optOutConfig = path.join(temporaryRoot, "opt-out")
|
|
118
153
|
const optOutLog = path.join(temporaryRoot, "opt-out.log")
|
|
119
154
|
await writeConfig(optOutConfig, ["@bybrawe/opencode-goal@1.3.16"])
|
|
@@ -146,10 +181,11 @@ try {
|
|
|
146
181
|
assert.match(help.stdout, /OpenCode Loop installer\/updater/)
|
|
147
182
|
assert.match(help.stdout, /--with-goals/)
|
|
148
183
|
assert.match(help.stdout, /--loop-only/)
|
|
184
|
+
assert.match(help.stdout, /--without-loop-goals/)
|
|
149
185
|
assert.match(help.stdout, /Loop uninstall never removes Goals/)
|
|
150
186
|
assert.deepEqual(await calls(helpLog), [], "help/version flows must never install companion packages")
|
|
151
187
|
|
|
152
188
|
console.log("OpenCode Loop Goals companion installer test passed")
|
|
153
189
|
} finally {
|
|
154
190
|
await fs.rm(temporaryRoot, { recursive: true, force: true })
|
|
155
|
-
}
|
|
191
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process"
|
|
3
|
-
import { readFile } from "node:fs/promises"
|
|
3
|
+
import { readFile, readdir, rm } from "node:fs/promises"
|
|
4
4
|
import { homedir } from "node:os"
|
|
5
5
|
import { dirname, join } from "node:path"
|
|
6
6
|
import process from "node:process"
|
|
@@ -16,7 +16,8 @@ const managedGoalCommandMarker = "<!-- managed-by:@bybrawe/opencode-goal -->"
|
|
|
16
16
|
const rawArgs = process.argv.slice(2)
|
|
17
17
|
const withGoals = rawArgs.includes("--with-goals")
|
|
18
18
|
const loopOnly = rawArgs.includes("--loop-only")
|
|
19
|
-
const
|
|
19
|
+
const withoutLoopGoals = rawArgs.includes("--without-loop-goals")
|
|
20
|
+
const loopArgs = rawArgs.filter((arg) => !["--with-goals", "--loop-only", "--without-loop-goals"].includes(arg))
|
|
20
21
|
const uninstallRequested = loopArgs.length === 1 && ["--uninstall", "uninstall", "--remove"].includes(loopArgs[0] || "")
|
|
21
22
|
const helpRequested = loopArgs.some((arg) => ["--help", "-h"].includes(arg))
|
|
22
23
|
const versionRequested = loopArgs.some((arg) => ["--version", "-v"].includes(arg))
|
|
@@ -117,6 +118,22 @@ async function goalsAlreadyInstalled() {
|
|
|
117
118
|
return false
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
async function removeLoopGoalCommands() {
|
|
122
|
+
const commandDir = join(config, "commands")
|
|
123
|
+
const names = (await readdir(join(root, "commands")))
|
|
124
|
+
.filter((name) => name === "loop-goal.md" || (name.startsWith("loop-goal-") && name.endsWith(".md")))
|
|
125
|
+
let removed = 0
|
|
126
|
+
for (const name of names) {
|
|
127
|
+
try {
|
|
128
|
+
await rm(join(commandDir, name))
|
|
129
|
+
removed++
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error?.code !== "ENOENT") throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return removed
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
function runLoopInstaller() {
|
|
121
138
|
return spawnSync(process.execPath, [loopInstaller, ...loopArgs], {
|
|
122
139
|
cwd: root,
|
|
@@ -148,7 +165,7 @@ function statusCode(result) {
|
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
function printCompanionHelp() {
|
|
151
|
-
console.log(`\nOpenCode Goals companion options:\n --with-goals
|
|
168
|
+
console.log(`\nOpenCode Goals companion options:\n --with-goals Install/update @bybrawe/opencode-goal@latest even when it is not already installed.\n --loop-only Skip all OpenCode Goals companion detection and network update work.\n --without-loop-goals Omit/remove Loop's packaged experimental /loop-goal* command files for this install/update.\n\nNormal Loop install/update refreshes an already-managed OpenCode Goals installation on a best-effort basis. Loop uninstall never removes Goals.`)
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
async function main() {
|
|
@@ -176,6 +193,11 @@ async function main() {
|
|
|
176
193
|
}
|
|
177
194
|
if (uninstallRequested) return
|
|
178
195
|
|
|
196
|
+
if (withoutLoopGoals) {
|
|
197
|
+
const removed = await removeLoopGoalCommands()
|
|
198
|
+
console.log(`Omitted ${removed} packaged experimental /loop-goal* command file(s) (--without-loop-goals).`)
|
|
199
|
+
}
|
|
200
|
+
|
|
179
201
|
if (loopOnly) {
|
|
180
202
|
console.log("Skipped OpenCode Goals companion update (--loop-only).")
|
|
181
203
|
return
|
|
@@ -205,4 +227,4 @@ async function main() {
|
|
|
205
227
|
main().catch((error) => {
|
|
206
228
|
console.error(error?.stack || error)
|
|
207
229
|
process.exitCode = 1
|
|
208
|
-
})
|
|
230
|
+
})
|