@bybrawe/opencode-loop 0.6.3 → 0.6.5
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 +16 -0
- package/README.md +4 -4
- package/examples/OPENCODE2-0.6.4.md +42 -0
- package/examples/OPENCODE2-0.6.5.md +42 -0
- package/package.json +2 -2
- package/scripts/comprehensive-test.mjs +7 -5
- package/scripts/install-node.mjs +82 -10
- package/scripts/native-guide-test.mjs +4 -1
- package/scripts/native-install-pair-test.mjs +6 -4
- package/scripts/native-install-test.mjs +24 -1
- package/scripts/native-release.mjs +3 -3
- package/scripts/publish-workflow-test.mjs +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.5 - 2026-10-02
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
- Pair the release with Goal 1.3.44, which carries the same SDK audit plus persisted execution-ownership recovery and current OpenTUI peer alignment.
|
|
9
|
+
|
|
10
|
+
## 0.6.4 - 2026-10-02
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- Mirror OpenCode 2.0.21 normalization semantics: strings remain strings and valid legacy `[package, options]` tuples become native `{ package, options }` entries in order ahead of existing native registrations.
|
|
14
|
+
- Preserve malformed/unrecognized legacy entries and unrelated provider/model configuration instead of deleting or rewriting user-owned settings.
|
|
15
|
+
- Pin `@opencode/plugin` to exact 2.0.18 so package resolution and CI/release builds cannot drift to a newer SDK with a different peer graph.
|
|
16
|
+
- Replace a 25 ms blocked-shell test race with deterministic state waiting and update Goal+Loop installer regressions for the canonical single-list configuration.
|
|
17
|
+
- Remove the redundant post-publish `npm view` cache check; the retrying direct registry verifier already proves exact version, gitHead and latest tag.
|
|
18
|
+
|
|
3
19
|
## 0.6.3 - 2026-10-02
|
|
4
20
|
|
|
5
21
|
- Fix native OpenCode 2 fresh installs on Windows/Linux by registering the exact published Loop package in plural `plugins` instead of copying a loose global `plugins/opencode-loop` directory.
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# OpenCode Loop
|
|
2
2
|
|
|
3
|
-
## Native OpenCode 2 (0.6.
|
|
3
|
+
## Native OpenCode 2 (0.6.5)
|
|
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
|
|
|
7
|
-
The installer defaults to the native V2 package plugin. It needs no loose local V2 copy, V1 SDK, or legacy command files.
|
|
7
|
+
The installer defaults to the native V2 package plugin. It needs no loose local V2 copy, V1 SDK, or legacy command files. Valid legacy `plugin` registrations are now persisted into the single native `plugins` list using OpenCode 2.0.21's own normalization semantics, so native installs no longer leave parallel `plugin` and `plugins` blocks. Object options and entry order are preserved, malformed legacy values are left untouched, and unrelated provider/model settings are not rewritten. Existing 0.6.1/0.6.2 loose `plugins/opencode-loop` installs are still removed on update. Use `--legacy-v1` explicitly for the compatibility installation.
|
|
8
8
|
|
|
9
9
|
V2 prompt admission and durable inbox/execution/compaction events own scheduling. Core-generated user-role messages never authorize a session abort. An unfinished dedicated Goal reserves its session, including paused and handed-off states; Loop does not wake or replace that Goal. Soft iteration timeouts do not abort a native model/tool/compaction turn.
|
|
10
10
|
|
|
@@ -17,7 +17,7 @@ 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.
|
|
20
|
+
> **Current stable release: `0.6.5`.**
|
|
21
21
|
|
|
22
22
|
## Install or update
|
|
23
23
|
|
|
@@ -42,7 +42,7 @@ This is the recommended split for OpenCode 2:
|
|
|
42
42
|
- `--loop-only` prevents the Loop installer from installing/updating the Goal companion.
|
|
43
43
|
- `--without-loop-goals` removes only Loop's older experimental `/loop-goal*` command files; it does not remove the dedicated OpenCode Goal plugin.
|
|
44
44
|
|
|
45
|
-
Run the same two commands again to update both packages. Loop 0.6.
|
|
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:
|
|
46
46
|
|
|
47
47
|
```text
|
|
48
48
|
/goal status
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# OpenCode Loop 0.6.4
|
|
2
|
+
|
|
3
|
+
OpenCode 2 configuration canonicalization patch, paired with OpenCode Goal 1.3.43.
|
|
4
|
+
|
|
5
|
+
## One native plugin list
|
|
6
|
+
|
|
7
|
+
OpenCode 2.0.21's runtime schema uses the plural `plugins` field and its
|
|
8
|
+
normalizer accepts the legacy singular `plugin` field for compatibility. Loop
|
|
9
|
+
now persists the same normalization during a native V2 install.
|
|
10
|
+
|
|
11
|
+
For valid legacy entries:
|
|
12
|
+
|
|
13
|
+
- strings stay strings;
|
|
14
|
+
- `[package, options]` tuples become `{ "package": "...", "options": { ... } }`;
|
|
15
|
+
- migrated legacy entries retain their order ahead of existing native entries;
|
|
16
|
+
- Loop's own registration is de-duplicated and pinned to the exact package version;
|
|
17
|
+
- the old `plugin` property is removed when it becomes empty.
|
|
18
|
+
|
|
19
|
+
Malformed or unrecognized legacy entries are preserved. Loop does not rewrite
|
|
20
|
+
unrelated `provider`, `providers`, `model`, or other user configuration.
|
|
21
|
+
|
|
22
|
+
## Deterministic runtime dependency
|
|
23
|
+
|
|
24
|
+
The production `@opencode/plugin` dependency is pinned to exact 2.0.18. This
|
|
25
|
+
prevents npm dependency drift from silently changing Loop's V2 runtime contract.
|
|
26
|
+
|
|
27
|
+
## Upgrade
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx -y @bybrawe/opencode-goal@1.3.43
|
|
31
|
+
npx -y @bybrawe/opencode-loop@0.6.4 --loop-only --without-loop-goals
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then fully restart OpenCode and verify:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
/goal status
|
|
38
|
+
/loop-help
|
|
39
|
+
/loop-doctor
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The update preserves Loop project state and user-authored command files.
|
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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/plugin.js",
|
|
@@ -75,6 +75,6 @@
|
|
|
75
75
|
"access": "public"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@opencode/plugin": "
|
|
78
|
+
"@opencode/plugin": "2.0.22"
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -348,12 +348,14 @@ async function testActionRoutingAndSafety() {
|
|
|
348
348
|
try {
|
|
349
349
|
await h.command("loop-shell", "0s --safe Remove-Item -Recurse ./important")
|
|
350
350
|
await h.command("loop-now", "shell")
|
|
351
|
-
await
|
|
351
|
+
const state = await waitForValue(async () => {
|
|
352
|
+
const candidate = await h.readState()
|
|
353
|
+
return candidate.jobs[0]?.paused === true && candidate.jobs[0]?.lastFailureReason === "safe_shell_blocked" ? candidate : undefined
|
|
354
|
+
}, 2_000)
|
|
352
355
|
assert.equal(h.records.shells.length, 0)
|
|
353
|
-
|
|
354
|
-
assert.equal(state
|
|
355
|
-
assert.equal(state
|
|
356
|
-
assert.equal(state.jobs[0].lastFailureReason, "safe_shell_blocked")
|
|
356
|
+
assert.equal(state?.jobs[0]?.paused, true, "a blocked synchronous action must pause instead of retrying forever")
|
|
357
|
+
assert.equal(state?.jobs[0]?.failureCount, 1)
|
|
358
|
+
assert.equal(state?.jobs[0]?.lastFailureReason, "safe_shell_blocked")
|
|
357
359
|
|
|
358
360
|
await h.command("loop-clear")
|
|
359
361
|
await h.command("loop-shell", "0s --safe rm -r -f ./important")
|
package/scripts/install-node.mjs
CHANGED
|
@@ -206,6 +206,53 @@ function findRootProperty(source, propertyName) {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
function removeRootProperty(source, propertyName) {
|
|
210
|
+
let index = skipTrivia(source, 0)
|
|
211
|
+
if (source[index] !== "{") throw new Error("OpenCode config must contain one root object")
|
|
212
|
+
index++
|
|
213
|
+
let previousComma
|
|
214
|
+
|
|
215
|
+
while (true) {
|
|
216
|
+
index = skipTrivia(source, index)
|
|
217
|
+
if (source[index] === "}") return source
|
|
218
|
+
|
|
219
|
+
const keyStart = index
|
|
220
|
+
const key = readJsonString(source, keyStart)
|
|
221
|
+
index = skipTrivia(source, key.end)
|
|
222
|
+
if (source[index] !== ":") throw new Error(`expected ':' after config property ${key.value}`)
|
|
223
|
+
const valueStart = skipTrivia(source, index + 1)
|
|
224
|
+
const valueEnd = skipJsonValue(source, valueStart)
|
|
225
|
+
const afterValue = skipTrivia(source, valueEnd)
|
|
226
|
+
const hadComma = source[afterValue] === ","
|
|
227
|
+
|
|
228
|
+
if (key.value === propertyName) {
|
|
229
|
+
if (hadComma) return `${source.slice(0, keyStart)}${source.slice(afterValue + 1)}`
|
|
230
|
+
if (previousComma !== undefined) return `${source.slice(0, previousComma)}${source.slice(valueEnd)}`
|
|
231
|
+
return `${source.slice(0, keyStart)}${source.slice(valueEnd)}`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (hadComma) {
|
|
235
|
+
previousComma = afterValue
|
|
236
|
+
index = afterValue + 1
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
if (source[afterValue] === "}") return source
|
|
240
|
+
throw new Error(`expected ',' or '}' after config property ${key.value}`)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function migrateLegacyPluginEntry(value) {
|
|
245
|
+
// Mirrors OpenCode 2.0.21 config normalization: strings stay strings and
|
|
246
|
+
// legacy [package, options] tuples become native { package, options } entries.
|
|
247
|
+
if (typeof value === "string") return value
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
if (value.length !== 2 || typeof value[0] !== "string" || !value[1] || typeof value[1] !== "object" || Array.isArray(value[1])) return undefined
|
|
250
|
+
return { package: value[0], options: value[1] }
|
|
251
|
+
}
|
|
252
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.package === "string") return value
|
|
253
|
+
return undefined
|
|
254
|
+
}
|
|
255
|
+
|
|
209
256
|
function formatPluginArray(values, indent, eol) {
|
|
210
257
|
if (!values.length) return "[]"
|
|
211
258
|
const childIndent = `${indent} `
|
|
@@ -263,11 +310,13 @@ async function configurePackagePlugin() {
|
|
|
263
310
|
for (const key of ["plugin", "plugins"]) {
|
|
264
311
|
if (parsed[key] !== undefined && !Array.isArray(parsed[key])) throw new Error(`OpenCode config '${key}' must be an array`)
|
|
265
312
|
}
|
|
313
|
+
|
|
266
314
|
let updated = source
|
|
267
315
|
const own = [...(parsed.plugins || []), ...(parsed.plugin || [])].filter(isPackageSpec)
|
|
268
|
-
if (own.length)
|
|
269
|
-
|
|
270
|
-
|
|
316
|
+
if (own.length) configured = true
|
|
317
|
+
|
|
318
|
+
if (legacyV1) {
|
|
319
|
+
if (own.length) {
|
|
271
320
|
for (const key of ["plugin", "plugins"]) {
|
|
272
321
|
const entries = parsed[key] || []
|
|
273
322
|
const matching = entries.filter(isPackageSpec)
|
|
@@ -275,17 +324,40 @@ async function configurePackagePlugin() {
|
|
|
275
324
|
const pinned = pinRegistration(matching)
|
|
276
325
|
updated = rewriteExistingPluginArray(updated, [...entries.filter((entry) => !isPackageSpec(entry)), pinned], key)
|
|
277
326
|
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
// Persist OpenCode 2.0.21's compatibility normalization so users do not
|
|
330
|
+
// accumulate parallel plugin/plugins blocks. Valid legacy entries move
|
|
331
|
+
// to native plugins in the same order; invalid values stay untouched.
|
|
332
|
+
const legacyEntries = parsed.plugin || []
|
|
333
|
+
const migratedLegacy = []
|
|
334
|
+
const retainedLegacy = []
|
|
335
|
+
for (const entry of legacyEntries.filter((entry) => !isPackageSpec(entry))) {
|
|
336
|
+
const migrated = migrateLegacyPluginEntry(entry)
|
|
337
|
+
if (migrated === undefined) retainedLegacy.push(entry)
|
|
338
|
+
else migratedLegacy.push(migrated)
|
|
339
|
+
}
|
|
340
|
+
if (legacyEntries.length) {
|
|
341
|
+
updated = retainedLegacy.length
|
|
342
|
+
? rewriteExistingPluginArray(updated, retainedLegacy, "plugin")
|
|
343
|
+
: removeRootProperty(updated, "plugin")
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const current = parseJsonc(updated)
|
|
347
|
+
const nativeEntries = current.plugins || []
|
|
348
|
+
if (!Array.isArray(nativeEntries)) throw new Error("OpenCode config 'plugins' must be an array")
|
|
349
|
+
const next = [
|
|
350
|
+
...migratedLegacy,
|
|
351
|
+
...nativeEntries.filter((entry) => !isPackageSpec(entry)),
|
|
352
|
+
...(own.length ? [pinRegistration(own)] : []),
|
|
353
|
+
]
|
|
354
|
+
if (migratedLegacy.length || own.length) {
|
|
355
|
+
updated = current.plugins === undefined
|
|
285
356
|
? appendPluginProperty(updated, next, "plugins")
|
|
286
357
|
: rewriteExistingPluginArray(updated, next, "plugins")
|
|
287
358
|
}
|
|
288
359
|
}
|
|
360
|
+
|
|
289
361
|
parseJsonc(updated)
|
|
290
362
|
plans.push({ target, source, updated })
|
|
291
363
|
} catch (error) {
|
|
@@ -24,7 +24,10 @@ try {
|
|
|
24
24
|
assert.deepEqual(JSON.parse(await readFile(file, "utf8")).plugin, [other, [spec, options]])
|
|
25
25
|
result = run()
|
|
26
26
|
assert.equal(result.status, 0, result.stderr)
|
|
27
|
-
assert.deepEqual(JSON.parse(await readFile(file, "utf8")), {
|
|
27
|
+
assert.deepEqual(JSON.parse(await readFile(file, "utf8")), { plugins: [
|
|
28
|
+
{ package: "other-plugin", options: { enabled: false } },
|
|
29
|
+
{ package: spec, options },
|
|
30
|
+
] })
|
|
28
31
|
const migrated = await readFile(file, "utf8")
|
|
29
32
|
assert.equal(run().status, 0)
|
|
30
33
|
assert.equal(await readFile(file, "utf8"), migrated)
|
|
@@ -47,8 +47,9 @@ try {
|
|
|
47
47
|
run(config, ["--with-goals", "--without-loop-goals"])
|
|
48
48
|
let source = await readFile(path.join(config, "opencode.json"), "utf8")
|
|
49
49
|
let parsed = JSON.parse(source)
|
|
50
|
-
assert.
|
|
51
|
-
assert.equal(parsed.plugins.length,
|
|
50
|
+
assert.equal("plugin" in parsed, false)
|
|
51
|
+
assert.equal(parsed.plugins.length, 3)
|
|
52
|
+
assert.ok(parsed.plugins.includes("other-plugin"))
|
|
52
53
|
assert.deepEqual(parsed.plugins.find((item) => item.package?.startsWith("@bybrawe/opencode-goal@")).options, options)
|
|
53
54
|
assert.deepEqual(parsed.plugins.find((item) => item.package?.startsWith("@bybrawe/opencode-loop@")).options, { quiet: true })
|
|
54
55
|
assert.equal(await readFile(path.join(config, "commands/goal.md"), "utf8"), "Custom Goal command\n")
|
|
@@ -66,8 +67,9 @@ try {
|
|
|
66
67
|
assert.deepEqual((await readFile(targetLog, "utf8")).trim().split('\n').map((line) => JSON.parse(line)), ["2.0.0", "2.0.0", "1.0.0"])
|
|
67
68
|
run(config, ["--uninstall"])
|
|
68
69
|
parsed = JSON.parse(await readFile(path.join(config, "opencode.json"), "utf8"))
|
|
69
|
-
assert.equal(parsed.plugins.length,
|
|
70
|
-
assert.ok(parsed.plugins
|
|
70
|
+
assert.equal(parsed.plugins.length, 2, "Loop uninstall cannot remove the companion or unrelated plugins")
|
|
71
|
+
assert.ok(parsed.plugins.includes("other-plugin"))
|
|
72
|
+
assert.ok(parsed.plugins.some((item) => item?.package?.startsWith("@bybrawe/opencode-goal@")))
|
|
71
73
|
assert.equal(await readFile(state, "utf8"), '{"evidence":"unchanged"}\n')
|
|
72
74
|
console.log("Native Goal+Loop installer pair: dialect, options, idempotence, custom files and uninstall PASS")
|
|
73
75
|
} finally { await rm(temp, { recursive: true, force: true }) }
|
|
@@ -64,7 +64,30 @@ try {
|
|
|
64
64
|
|
|
65
65
|
const both = await config("both", { plugin: ["legacy-unrelated", "@bybrawe/opencode-loop@old"], plugins: [options, "native-unrelated"] })
|
|
66
66
|
run(both)
|
|
67
|
-
assert.deepEqual(await readConfig(both), {
|
|
67
|
+
assert.deepEqual(await readConfig(both), { plugins: ["legacy-unrelated", "native-unrelated", { ...options, package: spec }] })
|
|
68
|
+
cases++
|
|
69
|
+
|
|
70
|
+
const userLegacy = await config("user-legacy", {
|
|
71
|
+
$schema: "https://opencode.ai/config.json",
|
|
72
|
+
plugin: [
|
|
73
|
+
"opencode-antigravity-auth@beta",
|
|
74
|
+
"@tarquinen/opencode-dcp@latest",
|
|
75
|
+
"opencode-power-pack@git+https://github.com/waybarrios/opencode-power-pack.git",
|
|
76
|
+
],
|
|
77
|
+
provider: { google: { models: { demo: { name: "Demo" } } } },
|
|
78
|
+
model: "",
|
|
79
|
+
})
|
|
80
|
+
run(userLegacy)
|
|
81
|
+
const canonicalUser = await readConfig(userLegacy)
|
|
82
|
+
assert.equal("plugin" in canonicalUser, false)
|
|
83
|
+
assert.deepEqual(canonicalUser.plugins, [
|
|
84
|
+
"opencode-antigravity-auth@beta",
|
|
85
|
+
"@tarquinen/opencode-dcp@latest",
|
|
86
|
+
"opencode-power-pack@git+https://github.com/waybarrios/opencode-power-pack.git",
|
|
87
|
+
spec,
|
|
88
|
+
])
|
|
89
|
+
assert.deepEqual(canonicalUser.provider, { google: { models: { demo: { name: "Demo" } } } })
|
|
90
|
+
assert.equal(canonicalUser.model, "")
|
|
68
91
|
cases++
|
|
69
92
|
|
|
70
93
|
const jsonc = await config("jsonc", '{\r\n // keep-before\r\n "theme": "url://example",\r\n "plugin": ["@bybrawe/opencode-loop@old"] // keep-after\r\n}\r\n')
|
|
@@ -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.
|
|
10
|
+
assert.equal(request?.version, "0.6.5", "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.
|
|
15
|
-
assert.equal(request.goal?.version, "1.3.
|
|
14
|
+
assert.equal(request.predecessor, "0.6.4")
|
|
15
|
+
assert.equal(request.goal?.version, "1.3.44")
|
|
16
16
|
assert.match(request.goal?.sha ?? "", /^[a-f0-9]{40}$/)
|
|
17
17
|
return request
|
|
18
18
|
}
|
|
@@ -24,12 +24,12 @@ assert.match(workflow, /windows-latest/)
|
|
|
24
24
|
assert.match(workflow, /Decide whether npm publish is needed/)
|
|
25
25
|
assert.match(workflow, /steps\.npm-state\.outputs\.publish == 'true'/)
|
|
26
26
|
assert.match(workflow, /for attempt in \{1\.\.60\}/)
|
|
27
|
-
assert.
|
|
27
|
+
assert.doesNotMatch(workflow, /npm view @bybrawe\/opencode-loop@latest/, "direct registry verification already checks latest; do not add a stale npm-view cache gate")
|
|
28
28
|
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\.
|
|
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.
|
|
47
|
+
const request = { name: "@bybrawe/opencode-loop", version: "0.6.5", predecessor: "0.6.4", goal: { version: "1.3.44", 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.
|
|
54
|
-
assert.equal(releaseDecision(404, null, request, sha, "0.6.
|
|
53
|
+
assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.4" }, env))
|
|
54
|
+
assert.equal(releaseDecision(404, null, request, sha, "0.6.4"), 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 }
|