@bybrawe/opencode-loop 0.6.4 → 0.6.6

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.6 - 2026-10-04
4
+
5
+ - Make a normal Loop install/update strictly Loop-only: it no longer installs, detects, refreshes, migrates, or modifies the dedicated `@bybrawe/opencode-goal` package.
6
+ - Omit Loop's legacy experimental `/loop-goal*` commands by default; retain them only behind explicit `--with-loop-goals` compatibility opt-in.
7
+ - Keep `--with-goals` as the explicit combined-install opt-in, while recommending the independently installable OpenCode Goal plugin for durable verified goals.
8
+ - Link Loop and Goal documentation directly so the two packages remain complementary without hidden installer coupling.
9
+
10
+ ## 0.6.5 - 2026-10-02
11
+
12
+ - Audit the native OpenCode 2 package contract against `@opencode/plugin@2.0.22` and pin the production SDK/runtime dependency to exact 2.0.22.
13
+ - Preserve the exact OpenCode 2.0.18 native pair as a backwards-compatibility gate while also requiring the moving latest-host pair; the real latest gate passes on OpenCode 2.0.22.
14
+ - Keep Ubuntu and Windows native bundle, regression, package-smoke, real-adapter, V1 peer-compatibility, and Loop+Goal coexistence gates green with the 2.0.22 dependency graph.
15
+ - Pair the release with Goal 1.3.44, which carries the same SDK audit plus persisted execution-ownership recovery and current OpenTUI peer alignment.
16
+
3
17
  ## 0.6.4 - 2026-10-02
4
18
 
5
19
  - Persist OpenCode 2 native plugin configuration as one canonical plural `plugins` list instead of leaving valid legacy `plugin` entries in a parallel top-level block.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenCode Loop
2
2
 
3
- ## Native OpenCode 2 (0.6.4)
3
+ ## Native OpenCode 2 (0.6.6)
4
4
 
5
5
  The npm and V2 entrypoints use the official `@opencode/plugin` `Plugin.define()` contract. The installer now registers the exact `@bybrawe/opencode-loop` package in OpenCode's plural `plugins` configuration instead of copying a loose global plugin directory. OpenCode therefore resolves `@opencode/plugin` from Loop's own production dependency graph; explicit `--legacy-v1` remains a separate compatibility path.
6
6
 
@@ -17,51 +17,55 @@ Prompt/command timers, watch and stop conditions, verification, preflight/postru
17
17
 
18
18
  OpenCode Loop adds `/loop`, scheduled prompt/command/shell jobs, compact scheduling, verification/checkpoints, and the `opencode-loopd` background daemon.
19
19
 
20
- > **Current stable release: `0.6.4`.**
20
+ > **Current stable release: `0.6.6`.**
21
21
 
22
22
  ## Install or update
23
23
 
24
24
  ### Recommended OpenCode 2 setup
25
25
 
26
- Install the dedicated Goal plugin first:
26
+ Install **both packages separately**:
27
27
 
28
28
  ```bash
29
+ npx -y @bybrawe/opencode-loop@latest
29
30
  npx -y @bybrawe/opencode-goal@latest
30
31
  ```
31
32
 
32
- Then install Loop by itself and disable Loop's older experimental Goal command files:
33
+ This is the recommended setup. The two installers stay independent:
34
+
35
+ - the Loop command installs or updates **OpenCode Loop only**;
36
+ - the Goal command installs or updates the dedicated **OpenCode Goal** plugin;
37
+ - Loop does **not** install, detect, refresh, migrate, or otherwise modify `@bybrawe/opencode-goal` during a normal install/update;
38
+ - Loop omits/removes its older experimental `/loop-goal*` command files by default.
39
+
40
+ OpenCode Goal owns `/goal`, durable objective state, evidence, verification, restart recovery, and semantic completion. See [OpenCode Goal](https://github.com/ByBrawe/opencode-goal).
41
+
42
+ A combined `--with-goals` path remains only as a compatibility/convenience option; the recommended setup is the two separate commands above:
43
+
44
+ ```bash
45
+ npx -y @bybrawe/opencode-loop@latest --with-goals
46
+ ```
47
+
48
+ If you explicitly need Loop's **legacy experimental** `/loop-goal*` commands for compatibility, opt in with:
33
49
 
34
50
  ```bash
35
- npx -y @bybrawe/opencode-loop@latest --loop-only --without-loop-goals
51
+ npx -y @bybrawe/opencode-loop@latest --with-loop-goals
36
52
  ```
37
53
 
38
- This is the recommended split for OpenCode 2:
54
+ Those legacy commands are not recommended for new setups; prefer the dedicated Goal plugin instead.
39
55
 
40
- - **OpenCode Goal** owns `/goal`, durable outcome-driven work, evidence, verification, recovery, and semantic completion.
41
- - **OpenCode Loop** owns `/loop`, scheduling, repeated prompts/commands/shell work, and `opencode-loopd`.
42
- - `--loop-only` prevents the Loop installer from installing/updating the Goal companion.
43
- - `--without-loop-goals` removes only Loop's older experimental `/loop-goal*` command files; it does not remove the dedicated OpenCode Goal plugin.
56
+ `--loop-only` remains as an explicit alias for the new default Loop-only behavior, and `--without-loop-goals` remains as a compatibility alias for the new default clean command surface.
44
57
 
45
- Run the same two commands again to update both packages. Loop 0.6.4 also canonicalizes valid legacy `plugin` entries into the native `plugins` list; earlier 0.6.1/0.6.2 loose global V2 installs are removed and replaced by the pinned published package. Then **fully restart OpenCode** and verify:
58
+ Run the relevant install command again to update that package. Then **fully restart OpenCode** and verify:
46
59
 
47
60
  ```text
48
- /goal status
49
61
  /loop-help
50
62
  /loop-doctor
51
63
  ```
52
64
 
53
- Convenience alternative: let the Loop installer also install/update the dedicated Goal companion:
65
+ If you installed Goal too:
54
66
 
55
- ```bash
56
- npx -y @bybrawe/opencode-loop@latest --with-goals --without-loop-goals
57
- ```
58
-
59
- For new OpenCode 2 installations, the explicit two-command setup above is preferred because ownership is clear and each package can be updated independently.
60
-
61
- Install only Loop without touching Goal:
62
-
63
- ```bash
64
- npx -y @bybrawe/opencode-loop@latest --loop-only
67
+ ```text
68
+ /goal status
65
69
  ```
66
70
 
67
71
  Global npm alternative:
@@ -84,7 +88,7 @@ opencode-loop --uninstall
84
88
  npm uninstall -g @bybrawe/opencode-loop
85
89
  ```
86
90
 
87
- Project Loop state under `.opencode/opencode-loop/` is intentionally preserved by uninstall.
91
+ Loop uninstall never removes the dedicated Goal plugin. Project Loop state under `.opencode/opencode-loop/` is intentionally preserved by uninstall.
88
92
 
89
93
  ## The mental model
90
94
 
@@ -374,7 +378,7 @@ Recommended choices are to finish/pause the Goal or use a separate session. Adva
374
378
 
375
379
  That escape hatch can create competing autonomous turns; use it only when that is intentional.
376
380
 
377
- Loop's older experimental `/loop-goal*` commands remain available for compatibility unless installed with `--without-loop-goals`. For new strongly verified Goal work, prefer:
381
+ Loop's older experimental `/loop-goal*` commands are omitted by default. They remain available only as an explicit compatibility opt-in with `--with-loop-goals`. For new strongly verified Goal work, use the separate [OpenCode Goal](https://github.com/ByBrawe/opencode-goal) plugin:
378
382
 
379
383
  ```bash
380
384
  npx -y @bybrawe/opencode-goal@latest
@@ -0,0 +1,42 @@
1
+ # OpenCode Loop 0.6.5
2
+
3
+ OpenCode 2.0.22 SDK-contract patch, paired with OpenCode Goal 1.3.44.
4
+
5
+ ## OpenCode 2.0.22 runtime contract
6
+
7
+ Loop's production `@opencode/plugin` dependency is pinned to exact 2.0.22. The
8
+ lockfile carries the matching `@opencode/{ai,client,protocol,schema,util}` runtime
9
+ closure so clean installs and release builds use the same native contract.
10
+
11
+ The moving real-host gate loads Loop main and the Goal companion together on
12
+ OpenCode 2.0.22. The exact OpenCode 2.0.18 native pair remains in CI as a
13
+ backwards-compatibility contract rather than being replaced by the current-host
14
+ check.
15
+
16
+ ## Validation
17
+
18
+ The release candidate is gated by:
19
+
20
+ - Ubuntu and Windows native bundle/regression/package-smoke jobs;
21
+ - the real OpenCode 2 adapter;
22
+ - exact OpenCode 2.0.18 Loop+Goal coexistence;
23
+ - latest OpenCode 2.0.22 Loop+Goal coexistence;
24
+ - OpenCode 1 peer-compatibility tests.
25
+
26
+ ## Upgrade
27
+
28
+ ```sh
29
+ npx -y @bybrawe/opencode-goal@1.3.44
30
+ npx -y @bybrawe/opencode-loop@0.6.5 --loop-only --without-loop-goals
31
+ ```
32
+
33
+ Then fully restart OpenCode and verify:
34
+
35
+ ```text
36
+ /goal status
37
+ /loop-help
38
+ /loop-doctor
39
+ ```
40
+
41
+ Loop state, installer canonicalization, V1 compatibility, and the host-owned
42
+ automatic compaction boundary are unchanged by this SDK patch.
@@ -0,0 +1,48 @@
1
+ # OpenCode Loop 0.6.6
2
+
3
+ Loop-only installation contract patch, paired for coexistence validation with OpenCode Goal 1.3.47.
4
+
5
+ ## Default installation
6
+
7
+ A normal Loop install/update now touches **Loop only**:
8
+
9
+ ```sh
10
+ npx -y @bybrawe/opencode-loop@0.6.6
11
+ ```
12
+
13
+ It does not install, detect, refresh, migrate, or modify the dedicated OpenCode Goal package. Loop's older experimental `/loop-goal*` command files are omitted by default.
14
+
15
+ For durable outcome-driven work, install Goal independently:
16
+
17
+ ```sh
18
+ npx -y @bybrawe/opencode-goal@1.3.47
19
+ ```
20
+
21
+ Or explicitly opt into a combined install:
22
+
23
+ ```sh
24
+ npx -y @bybrawe/opencode-loop@0.6.6 --with-goals
25
+ ```
26
+
27
+ Legacy experimental Loop Goal commands remain available only when explicitly requested:
28
+
29
+ ```sh
30
+ npx -y @bybrawe/opencode-loop@0.6.6 --with-loop-goals
31
+ ```
32
+
33
+ ## Validation contract
34
+
35
+ The release remains gated by Ubuntu/Windows regressions, production package smoke, the real OpenCode 2 adapter, exact OpenCode 2.0.18 compatibility, current-host coexistence, and immutable Goal companion source verification.
36
+
37
+ After installation, fully restart OpenCode and verify:
38
+
39
+ ```text
40
+ /loop-help
41
+ /loop-doctor
42
+ ```
43
+
44
+ If Goal is installed too:
45
+
46
+ ```text
47
+ /goal status
48
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bybrawe/opencode-loop",
3
- "version": "0.6.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.",
3
+ "version": "0.6.6",
4
+ "description": "Idle-safe /loop scheduling and background automation for OpenCode, with optional legacy Loop Goal compatibility and a separately installable OpenCode Goal companion.",
5
5
  "type": "module",
6
6
  "main": "src/plugin.js",
7
7
  "exports": {
@@ -75,6 +75,6 @@
75
75
  "access": "public"
76
76
  },
77
77
  "dependencies": {
78
- "@opencode/plugin": "2.0.18"
78
+ "@opencode/plugin": "2.0.22"
79
79
  }
80
80
  }
@@ -94,8 +94,8 @@ try {
94
94
  const absentLog = path.join(temporaryRoot, "absent.log")
95
95
  const absent = await runInstaller(absentConfig, [], { FAKE_NPM_LOG: absentLog })
96
96
  assert.equal(absent.code, 0, absent.stderr)
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")
97
+ assert.deepEqual(await calls(absentLog), [], "default Loop installs must not silently add or update Goal")
98
+ assert.deepEqual(await loopGoalCommands(absentConfig), [], "legacy Loop Goal commands must be omitted by default")
99
99
 
100
100
  const withoutLoopGoalsConfig = path.join(temporaryRoot, "without-loop-goals")
101
101
  const withoutLoopGoalsLog = path.join(temporaryRoot, "without-loop-goals.log")
@@ -111,15 +111,15 @@ try {
111
111
  await writeConfig(existingConfig, ["other-plugin", "@bybrawe/opencode-goal@1.3.16"])
112
112
  const existing = await runInstaller(existingConfig, [], { FAKE_NPM_LOG: existingLog })
113
113
  assert.equal(existing.code, 0, existing.stderr)
114
- assert.deepEqual(await calls(existingLog), [expectedGoalExec], "an installed Goals package must auto-refresh through its own latest installer")
115
- assert.match(existing.stdout, /Installed\/updated OpenCode Goals via @bybrawe\/opencode-goal@latest/)
114
+ assert.deepEqual(await calls(existingLog), [], "default Loop updates must not refresh an installed Goal package")
115
+ assert.match(existing.stdout, /Recommended companion install \(separate command\)/)
116
116
 
117
117
  const localConfig = path.join(temporaryRoot, "local-goal")
118
118
  const localLog = path.join(temporaryRoot, "local-goal.log")
119
119
  await writeConfig(localConfig, ["./plugins/opencode-goal.ts"])
120
120
  const local = await runInstaller(localConfig, [], { FAKE_NPM_LOG: localLog })
121
121
  assert.equal(local.code, 0, local.stderr)
122
- assert.deepEqual(await calls(localLog), [expectedGoalExec], "legacy local Goals installs must be refreshed/migrated by the companion installer")
122
+ assert.deepEqual(await calls(localLog), [], "default Loop updates must not migrate or refresh legacy Goal installs")
123
123
 
124
124
  const managedCommandConfig = path.join(temporaryRoot, "managed-command")
125
125
  const managedCommandLog = path.join(temporaryRoot, "managed-command.log")
@@ -127,7 +127,7 @@ try {
127
127
  await fs.writeFile(path.join(managedCommandConfig, "commands", "goal.md"), "<!-- managed-by:@bybrawe/opencode-goal -->\n", "utf8")
128
128
  const managedCommand = await runInstaller(managedCommandConfig, [], { FAKE_NPM_LOG: managedCommandLog })
129
129
  assert.equal(managedCommand.code, 0, managedCommand.stderr)
130
- assert.deepEqual(await calls(managedCommandLog), [expectedGoalExec], "a managed /goal command is sufficient evidence that Goals should be refreshed")
130
+ assert.deepEqual(await calls(managedCommandLog), [], "a managed /goal command must not trigger Goal network work during a default Loop update")
131
131
 
132
132
  const commentedConfig = path.join(temporaryRoot, "comment-only")
133
133
  const commentedLog = path.join(temporaryRoot, "comment-only.log")
@@ -149,21 +149,27 @@ try {
149
149
  assert.deepEqual(await calls(explicitCleanLog), [expectedGoalExec], "the clean command surface option must compose with --with-goals")
150
150
  assert.deepEqual(await loopGoalCommands(explicitCleanConfig), [], "combined install must keep the stronger Goals plugin without legacy Loop Goal command clutter")
151
151
 
152
+ const legacyGoalConfig = path.join(temporaryRoot, "legacy-loop-goals")
153
+ const legacyGoalLog = path.join(temporaryRoot, "legacy-loop-goals.log")
154
+ const legacyGoal = await runInstaller(legacyGoalConfig, ["--with-loop-goals"], { FAKE_NPM_LOG: legacyGoalLog })
155
+ assert.equal(legacyGoal.code, 0, legacyGoal.stderr)
156
+ assert.deepEqual(await calls(legacyGoalLog), [], "--with-loop-goals must not install the dedicated Goal package")
157
+ assert.ok((await loopGoalCommands(legacyGoalConfig)).length > 0, "--with-loop-goals must explicitly restore the legacy experimental command surface")
158
+
152
159
  const optOutConfig = path.join(temporaryRoot, "opt-out")
153
160
  const optOutLog = path.join(temporaryRoot, "opt-out.log")
154
161
  await writeConfig(optOutConfig, ["@bybrawe/opencode-goal@1.3.16"])
155
162
  const optOut = await runInstaller(optOutConfig, ["--loop-only"], { FAKE_NPM_LOG: optOutLog })
156
163
  assert.equal(optOut.code, 0, optOut.stderr)
157
164
  assert.deepEqual(await calls(optOutLog), [], "--loop-only must suppress companion network work")
158
- assert.match(optOut.stdout, /Skipped OpenCode Goals companion update/)
165
+ assert.match(optOut.stdout, /Installed\/updated OpenCode Loop only \(--loop-only\)/)
159
166
 
160
- const autoFailureConfig = path.join(temporaryRoot, "auto-failure")
161
- const autoFailureLog = path.join(temporaryRoot, "auto-failure.log")
162
- await writeConfig(autoFailureConfig, ["@bybrawe/opencode-goal@1.3.16"])
163
- const autoFailure = await runInstaller(autoFailureConfig, [], { FAKE_NPM_LOG: autoFailureLog, FAKE_NPM_EXIT: "7" })
164
- assert.equal(autoFailure.code, 0, "a best-effort refresh failure must not break an otherwise successful Loop update")
165
- assert.match(autoFailure.stderr, /could not be refreshed/)
166
- assert.match(autoFailure.stderr, /npx -y @bybrawe\/opencode-goal@latest/)
167
+ const untouchedConfig = path.join(temporaryRoot, "untouched-existing-goal")
168
+ const untouchedLog = path.join(temporaryRoot, "untouched-existing-goal.log")
169
+ await writeConfig(untouchedConfig, ["@bybrawe/opencode-goal@1.3.16"])
170
+ const untouched = await runInstaller(untouchedConfig, [], { FAKE_NPM_LOG: untouchedLog, FAKE_NPM_EXIT: "7" })
171
+ assert.equal(untouched.code, 0, untouched.stderr)
172
+ assert.deepEqual(await calls(untouchedLog), [], "default Loop update must perform zero Goal network work even when Goal is already installed")
167
173
 
168
174
  const explicitFailureConfig = path.join(temporaryRoot, "explicit-failure")
169
175
  const explicitFailureLog = path.join(temporaryRoot, "explicit-failure.log")
@@ -181,8 +187,10 @@ try {
181
187
  assert.match(help.stdout, /OpenCode Loop installer\/updater/)
182
188
  assert.match(help.stdout, /--with-goals/)
183
189
  assert.match(help.stdout, /--loop-only/)
190
+ assert.match(help.stdout, /--with-loop-goals/)
184
191
  assert.match(help.stdout, /--without-loop-goals/)
185
- assert.match(help.stdout, /Loop uninstall never removes Goals/)
192
+ assert.match(help.stdout, /Default install\/update touches only OpenCode Loop/)
193
+ assert.match(help.stdout, /Loop uninstall never removes Goal/)
186
194
  assert.deepEqual(await calls(helpLog), [], "help/version flows must never install companion packages")
187
195
 
188
196
  console.log("OpenCode Loop Goals companion installer test passed")
@@ -9,118 +9,19 @@ import { fileURLToPath } from "node:url"
9
9
  const root = dirname(dirname(fileURLToPath(import.meta.url)))
10
10
  const loopInstaller = join(root, "scripts", "install-node.mjs")
11
11
  const config = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode")
12
- const configCandidates = ["opencode.json", "opencode.jsonc", "config.json", "config.jsonc"]
13
- const goalPackageName = "@bybrawe/opencode-goal"
14
- const goalPackageSpec = `${goalPackageName}@latest`
15
- const managedGoalCommandMarker = "<!-- managed-by:@bybrawe/opencode-goal -->"
12
+ const goalPackageSpec = "@bybrawe/opencode-goal@latest"
16
13
  const rawArgs = process.argv.slice(2)
17
14
  const withGoals = rawArgs.includes("--with-goals")
18
15
  const loopOnly = rawArgs.includes("--loop-only")
16
+ const withLoopGoals = rawArgs.includes("--with-loop-goals")
19
17
  const withoutLoopGoals = rawArgs.includes("--without-loop-goals")
20
- const loopArgs = rawArgs.filter((arg) => !["--with-goals", "--loop-only", "--without-loop-goals"].includes(arg))
18
+ const loopArgs = rawArgs.filter((arg) => !["--with-goals", "--loop-only", "--with-loop-goals", "--without-loop-goals"].includes(arg))
21
19
  const commandArgs = loopArgs.filter((arg) => arg !== "--legacy-v1")
22
20
  const uninstallRequested = commandArgs.length === 1 && ["--uninstall", "uninstall", "--remove"].includes(commandArgs[0] || "")
23
21
  const helpRequested = loopArgs.some((arg) => ["--help", "-h"].includes(arg))
24
22
  const versionRequested = loopArgs.some((arg) => ["--version", "-v"].includes(arg))
25
23
  const informational = helpRequested || versionRequested
26
24
 
27
- function stripJsonComments(input) {
28
- let output = ""
29
- let quote = ""
30
- let escaped = false
31
- let lineComment = false
32
- let blockComment = false
33
- for (let index = 0; index < input.length; index++) {
34
- const char = input[index]
35
- const next = input[index + 1]
36
- if (lineComment) {
37
- if (char === "\n" || char === "\r") { lineComment = false; output += char }
38
- continue
39
- }
40
- if (blockComment) {
41
- if (char === "*" && next === "/") { blockComment = false; index++ }
42
- else if (char === "\n" || char === "\r") output += char
43
- continue
44
- }
45
- if (quote) {
46
- output += char
47
- if (escaped) escaped = false
48
- else if (char === "\\") escaped = true
49
- else if (char === quote) quote = ""
50
- continue
51
- }
52
- if (char === '"') { quote = char; output += char; continue }
53
- if (char === "/" && next === "/") { lineComment = true; index++; continue }
54
- if (char === "/" && next === "*") { blockComment = true; index++; continue }
55
- output += char
56
- }
57
- return output
58
- }
59
-
60
- function stripTrailingCommas(input) {
61
- let output = ""
62
- let quote = ""
63
- let escaped = false
64
- for (let index = 0; index < input.length; index++) {
65
- const char = input[index]
66
- if (quote) {
67
- output += char
68
- if (escaped) escaped = false
69
- else if (char === "\\") escaped = true
70
- else if (char === quote) quote = ""
71
- continue
72
- }
73
- if (char === '"') { quote = char; output += char; continue }
74
- if (char === ",") {
75
- let lookahead = index + 1
76
- while (/\s/.test(input[lookahead] || "")) lookahead++
77
- if (input[lookahead] === "]" || input[lookahead] === "}") continue
78
- }
79
- output += char
80
- }
81
- return output
82
- }
83
-
84
- function parseJsonc(input) {
85
- const parsed = JSON.parse(stripTrailingCommas(stripJsonComments(input)))
86
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenCode config root must be a JSON object")
87
- return parsed
88
- }
89
-
90
- function isGoalPluginSpec(value) {
91
- if (Array.isArray(value)) value = value[0]
92
- if (value && typeof value === "object") value = value.package
93
- if (typeof value !== "string") return false
94
- const spec = value.trim()
95
- if (spec === goalPackageName || spec.startsWith(`${goalPackageName}@`)) return true
96
- const normalized = spec.replaceAll("\\", "/")
97
- return normalized === "./plugins/opencode-goal.ts"
98
- || normalized === "./plugins/opencode-goal.js"
99
- || normalized.endsWith("/plugins/opencode-goal.ts")
100
- || normalized.endsWith("/plugins/opencode-goal.js")
101
- }
102
-
103
- async function goalsAlreadyInstalled() {
104
- for (const name of configCandidates) {
105
- try {
106
- const parsed = parseJsonc(await readFile(join(config, name), "utf8"))
107
- if ([parsed.plugin, parsed.plugins].some((entries) => Array.isArray(entries) && entries.some(isGoalPluginSpec))) return true
108
- } catch (error) {
109
- if (error?.code !== "ENOENT") {
110
- console.warn(`Could not inspect ${join(config, name)} for OpenCode Goals: ${error.message}`)
111
- }
112
- }
113
- }
114
-
115
- try {
116
- const command = await readFile(join(config, "commands", "goal.md"), "utf8")
117
- return command.includes(managedGoalCommandMarker)
118
- } catch (error) {
119
- if (error?.code !== "ENOENT") console.warn(`Could not inspect managed /goal command: ${error.message}`)
120
- }
121
- return false
122
- }
123
-
124
25
  async function removeLoopGoalCommands() {
125
26
  const commandDir = join(config, "commands")
126
27
  const names = (await readdir(join(root, "commands")))
@@ -178,7 +79,7 @@ function statusCode(result) {
178
79
  }
179
80
 
180
81
  function printCompanionHelp() {
181
- 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.`)
82
+ console.log(`\nOpenCode Goal options:\n --with-goals Explicitly install/update @bybrawe/opencode-goal@latest after Loop.\n --loop-only Explicit alias for the default: install/update Loop only and never touch Goal.\n --with-loop-goals Also install Loop's legacy experimental /loop-goal* command files (not recommended).\n --without-loop-goals Compatibility alias for the default: omit/remove legacy /loop-goal* commands.\n\nDefault install/update touches only OpenCode Loop. Recommended setup: install OpenCode Goal separately with: npx -y @bybrawe/opencode-goal@latest. Loop uninstall never removes Goal.`)
182
83
  }
183
84
 
184
85
  async function main() {
@@ -187,6 +88,11 @@ async function main() {
187
88
  process.exitCode = 2
188
89
  return
189
90
  }
91
+ if (withLoopGoals && withoutLoopGoals) {
92
+ console.error("Use either --with-loop-goals or --without-loop-goals, not both.")
93
+ process.exitCode = 2
94
+ return
95
+ }
190
96
 
191
97
  const loopResult = runLoopInstaller()
192
98
  if (loopResult.error) {
@@ -206,19 +112,18 @@ async function main() {
206
112
  }
207
113
  if (uninstallRequested) return
208
114
 
209
- if (withoutLoopGoals) {
115
+ if (!withLoopGoals) {
210
116
  const removed = await removeLoopGoalCommands()
211
- console.log(`Omitted ${removed} packaged experimental /loop-goal* command file(s) (--without-loop-goals).`)
117
+ const reason = withoutLoopGoals ? "--without-loop-goals" : "default Loop-only command surface"
118
+ console.log(`Omitted ${removed} packaged experimental /loop-goal* command file(s) (${reason}).`)
212
119
  }
213
120
 
214
- if (loopOnly) {
215
- console.log("Skipped OpenCode Goals companion update (--loop-only).")
121
+ if (!withGoals) {
122
+ if (loopOnly) console.log("Installed/updated OpenCode Loop only (--loop-only).")
123
+ else console.log("Installed/updated OpenCode Loop only. Recommended companion install (separate command): npx -y @bybrawe/opencode-goal@latest")
216
124
  return
217
125
  }
218
126
 
219
- const alreadyInstalled = await goalsAlreadyInstalled()
220
- if (!alreadyInstalled && !withGoals) return
221
-
222
127
  const goalResult = runGoalInstaller()
223
128
  const goalCode = statusCode(goalResult)
224
129
  if (goalCode === 0) {
@@ -227,14 +132,8 @@ async function main() {
227
132
  }
228
133
 
229
134
  const detail = goalResult.error?.message || `exit code ${goalCode}`
230
- if (withGoals) {
231
- console.error(`OpenCode Goals companion install/update failed (${detail}).`)
232
- process.exitCode = goalCode || 1
233
- return
234
- }
235
-
236
- console.warn(`OpenCode Loop updated, but the existing OpenCode Goals companion could not be refreshed (${detail}).`)
237
- console.warn(`Retry later with: npx -y ${goalPackageSpec}`)
135
+ console.error(`OpenCode Goals companion install/update failed (${detail}).`)
136
+ process.exitCode = goalCode || 1
238
137
  }
239
138
 
240
139
  main().catch((error) => {
@@ -7,12 +7,12 @@ export function validateRelease(request, pkg, env) {
7
7
  assert.equal(env.GITHUB_REPOSITORY, "ByBrawe/opencode-loop", "wrong release repository")
8
8
  assert.equal(env.GITHUB_REF, "refs/heads/main", "npm release must come from main")
9
9
  assert.match(env.GITHUB_SHA ?? "", /^[a-f0-9]{40}$/, "immutable release SHA required")
10
- assert.equal(request?.version, "0.6.4", "release version is not authorized")
10
+ assert.equal(request?.version, "0.6.6", "release version is not authorized")
11
11
  assert.equal(request?.name, "@bybrawe/opencode-loop")
12
12
  assert.equal(pkg.name, request.name)
13
13
  assert.equal(pkg.version, request.version)
14
- assert.equal(request.predecessor, "0.6.3")
15
- assert.equal(request.goal?.version, "1.3.43")
14
+ assert.equal(request.predecessor, "0.6.5")
15
+ assert.equal(request.goal?.version, "1.3.47")
16
16
  assert.match(request.goal?.sha ?? "", /^[a-f0-9]{40}$/)
17
17
  return request
18
18
  }
@@ -29,7 +29,7 @@ assert.match(workflow, /native-release\.mjs verify/)
29
29
  assert.match(workflow, /node_modules\/@bybrawe\/opencode-loop\/scripts\/opencode2-loop-canary.mjs/)
30
30
  assert.match(workflow, /node_modules\/@bybrawe\/opencode-goal\/dist\/server.js/)
31
31
  assert.match(workflow, /ref: \$\{\{ steps.release.outputs.goal_sha \}\}/)
32
- assert.match(workflow, /npm install --no-save --legacy-peer-deps @opencode\/plugin@2\.0\.18/, "immutable Goal companion must build against its pinned OpenCode 2.0.18 plugin contract")
32
+ assert.match(workflow, /npm install --no-save --legacy-peer-deps @opencode\/plugin@2\.0\.22/, "immutable Goal companion must build against its pinned OpenCode 2.0.22 plugin contract")
33
33
  assert.match(nativeV2Workflow, /npm install --no-save --legacy-peer-deps @opencode\/plugin@2\.0\.18/, "Native V2 CI must pin the immutable Goal companion to the same OpenCode 2.0.18 plugin contract")
34
34
  assert.doesNotMatch(workflow, /secrets\.NPM_TOKEN/)
35
35
 
@@ -44,14 +44,14 @@ const publisher = workflow.slice(workflow.indexOf(" publish:\n"), workflow.inde
44
44
  assert.doesNotMatch(publisher, /contents:\s*write/, "OIDC publisher must not gain repository mutation privileges")
45
45
 
46
46
  const sha = "a".repeat(40)
47
- const request = { name: "@bybrawe/opencode-loop", version: "0.6.4", predecessor: "0.6.3", goal: { version: "1.3.43", sha: "b".repeat(40) } }
47
+ const request = { name: "@bybrawe/opencode-loop", version: "0.6.6", predecessor: "0.6.5", goal: { version: "1.3.47", sha: "b".repeat(40) } }
48
48
  const pkg = { name: request.name, version: request.version }
49
49
  const env = { GITHUB_REPOSITORY: "ByBrawe/opencode-loop", GITHUB_REF: "refs/heads/main", GITHUB_SHA: sha }
50
50
  assert.strictEqual(validateRelease(request, pkg, env), request)
51
51
  for (const change of [{ GITHUB_REPOSITORY: "someone/fork" }, { GITHUB_REF: "refs/heads/release" }, { GITHUB_SHA: "main" }]) assert.throws(() => validateRelease(request, pkg, { ...env, ...change }))
52
52
  assert.throws(() => validateRelease({ ...request, version: "9.0.0" }, pkg, env))
53
- assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.3" }, env))
54
- assert.equal(releaseDecision(404, null, request, sha, "0.6.3"), true)
53
+ assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.5" }, env))
54
+ assert.equal(releaseDecision(404, null, request, sha, "0.6.5"), true)
55
55
  for (const status of [401, 403, 429, 500, 503]) assert.throws(() => releaseDecision(status, null, request, sha, "0.6.0"))
56
56
  assert.throws(() => releaseDecision(404, null, request, sha, "0.7.0"))
57
57
  const manifest = { ...pkg, gitHead: sha }