From cf9aefa083a36f73f4415c7a294184345b5d69ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:20:25 +0300 Subject: [PATCH 1/5] chore: stage v0.5.22 compatibility patch --- scripts/apply-v0522.py | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 scripts/apply-v0522.py diff --git a/scripts/apply-v0522.py b/scripts/apply-v0522.py new file mode 100644 index 00000000..00534320 --- /dev/null +++ b/scripts/apply-v0522.py @@ -0,0 +1,76 @@ +from pathlib import Path +import json + + +def replace_once(path, old, new): + p = Path(path) + text = p.read_text() + count = text.count(old) + if count != 1: + raise SystemExit(f"{path}: expected one match, found {count}") + p.write_text(text.replace(old, new, 1)) + + +replace_once( + "src/index.js", + ''' const handled = () => { + // OpenCode server plugins cannot currently cancel the command prompt turn. + // Keep the command markdown acknowledgement intact so opencode-loop-local + // receives a valid, tool-denied message instead of an empty parts array. + return true + } +''', + ''' const handled = () => { + // OpenCode 1.18.x ignores unknown hook output fields, while proposed/newer + // hosts can honor noReply to skip the otherwise unavoidable model turn. + // Keep acknowledgement parts intact as the compatibility fallback. + if (output && typeof output === "object") output.noReply = true + return true + } +''', +) + +replace_once( + "scripts/smoke-test.mjs", + ''' assert.equal(output.parts.length, 1, "a locally handled slash command must keep a valid acknowledgement prompt") + assert.equal(output.parts[0].text, "original command body") +''', + ''' assert.equal(output.parts.length, 1, "a locally handled slash command must keep a valid acknowledgement prompt") + assert.equal(output.parts[0].text, "original command body") + assert.equal(output.noReply, true, "handled commands should request noReply for compatible OpenCode hosts") +''', +) + +package_path = Path("package.json") +package = json.loads(package_path.read_text()) +if package.get("version") != "0.5.21": + raise SystemExit(f"unexpected package version: {package.get('version')}") +package["version"] = "0.5.22" +package["devDependencies"]["@opencode-ai/plugin"] = "^1.18.15" +package_path.write_text(json.dumps(package, indent=2) + "\n") + +changelog = Path("CHANGELOG.md") +text = changelog.read_text() +entry = '''## 0.5.22 + +- Hardened compatibility with current OpenCode 1.18.15 while preserving the existing `>=1.4.0` peer range. +- Handled loop commands now also set `output.noReply = true`; OpenCode 1.18.x safely ignores the unknown field, while hosts that add the proposed `noReply` hook support can skip the acknowledgement model turn automatically. +- Added Bun runtime loading coverage so CI exercises the runtime OpenCode actually uses to load plugins, not only Node syntax/tests. +- Added explicit minimum-peer coverage against `@opencode-ai/plugin@1.4.0` and scheduled testing against the latest published OpenCode plugin package. +- Kept the v0.5.21 server-plugin acknowledgement fallback for current OpenCode versions where `command.execute.before` cannot cancel the prompt turn. + +''' +if not text.startswith("# Changelog\n\n"): + raise SystemExit("unexpected changelog header") +changelog.write_text("# Changelog\n\n" + entry + text[len("# Changelog\n\n"):]) + +readme = Path("README.md") +text = readme.read_text() +marker = "**v0.5.21 targets current OpenCode 1.18.x compatibility and safer releases.**" +if marker not in text: + raise SystemExit("README v0.5.21 marker missing") +replacement = ( + "**v0.5.22 adds forward-compatible OpenCode command handling plus Bun and peer-range compatibility CI.** " + + marker +) +readme.write_text(text.replace(marker, replacement, 1)) From 1825b3f091dc3360e8b7f030442d1a7efefbe5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:20:35 +0300 Subject: [PATCH 2/5] ci: verify v0.5.22 candidate --- .github/workflows/apply-v0522-pr.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/apply-v0522-pr.yml diff --git a/.github/workflows/apply-v0522-pr.yml b/.github/workflows/apply-v0522-pr.yml new file mode 100644 index 00000000..86e3c2e1 --- /dev/null +++ b/.github/workflows/apply-v0522-pr.yml @@ -0,0 +1,44 @@ +name: Apply v0.5.22 on PR + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + apply: + if: github.head_ref == 'release/v0.5.22-compat-hardening' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + - uses: actions/setup-node@v6 + with: + node-version: "24" + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Apply compatibility patch + run: python scripts/apply-v0522.py + - name: Refresh lockfile + run: npm install --package-lock-only --ignore-scripts + - name: Verify Node suite and Bun loading + run: | + npm ci + npm run check + npm test + bun -e "await import('./src/index.js')" + npm pack --dry-run + - name: Commit verified code candidate + shell: bash + run: | + rm scripts/apply-v0522.py + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add src/index.js scripts/smoke-test.mjs package.json package-lock.json CHANGELOG.md README.md scripts/apply-v0522.py + git commit -m "feat: harden OpenCode compatibility for v0.5.22" + git push origin HEAD:${{ github.head_ref }} From 4bc926f983a6e456c91eda736f46238d0c3102a3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:21:11 +0000 Subject: [PATCH 3/5] feat: harden OpenCode compatibility for v0.5.22 --- CHANGELOG.md | 8 +++++ README.md | 2 +- package-lock.json | 4 +-- package.json | 2 +- scripts/apply-v0522.py | 76 ------------------------------------------ scripts/smoke-test.mjs | 1 + src/index.js | 7 ++-- 7 files changed, 17 insertions(+), 83 deletions(-) delete mode 100644 scripts/apply-v0522.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b5a56ec3..792ecc5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.5.22 + +- Hardened compatibility with current OpenCode 1.18.15 while preserving the existing `>=1.4.0` peer range. +- Handled loop commands now also set `output.noReply = true`; OpenCode 1.18.x safely ignores the unknown field, while hosts that add the proposed `noReply` hook support can skip the acknowledgement model turn automatically. +- Added Bun runtime loading coverage so CI exercises the runtime OpenCode actually uses to load plugins, not only Node syntax/tests. +- Added explicit minimum-peer coverage against `@opencode-ai/plugin@1.4.0` and scheduled testing against the latest published OpenCode plugin package. +- Kept the v0.5.21 server-plugin acknowledgement fallback for current OpenCode versions where `command.execute.before` cannot cancel the prompt turn. + ## 0.5.21 - Verified server-plugin compatibility against OpenCode 1.18.15 and updated the development plugin dependency accordingly. diff --git a/README.md b/README.md index bba6783a..b3f172fb 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ v0.5.11 includes a referenced heartbeat scheduler. This is important in OpenCode ## Current status -**v0.5.21 targets current OpenCode 1.18.x compatibility and safer releases.** Server-plugin control commands keep their locked-down acknowledgement prompt instead of creating an empty command message, `/compact` prefers the current `session.compact` TUI command, and CI now covers Ubuntu and Windows before publishing. **v0.5.20 fixes Windows TUI state writes.** Session job state is written through the OS temp directory with rename plus copy/unlink fallback and short retries, so antivirus locks and OpenCode snapshots no longer drop `/loop` jobs with `EPERM` on rename. **v0.5.19** hardens Goal Mode, package updates, and the background daemon: package installs are pinned to the installed version so OpenCode cannot keep loading an older cached release, scheduler-created goal messages no longer self-interrupt on delayed updates, finite daemon failures return nonzero, model/agent selection is supported, Windows scheduled tasks use a short launcher that stays below the `/TR` limit, and asynchronous release verification is reliable under load. +**v0.5.22 adds forward-compatible OpenCode command handling plus Bun and peer-range compatibility CI.** **v0.5.21 targets current OpenCode 1.18.x compatibility and safer releases.** Server-plugin control commands keep their locked-down acknowledgement prompt instead of creating an empty command message, `/compact` prefers the current `session.compact` TUI command, and CI now covers Ubuntu and Windows before publishing. **v0.5.20 fixes Windows TUI state writes.** Session job state is written through the OS temp directory with rename plus copy/unlink fallback and short retries, so antivirus locks and OpenCode snapshots no longer drop `/loop` jobs with `EPERM` on rename. **v0.5.19** hardens Goal Mode, package updates, and the background daemon: package installs are pinned to the installed version so OpenCode cannot keep loading an older cached release, scheduler-created goal messages no longer self-interrupt on delayed updates, finite daemon failures return nonzero, model/agent selection is supported, Windows scheduled tasks use a short launcher that stays below the `/TR` limit, and asynchronous release verification is reliable under load. The known update-related symptoms from older builds are fixed: diff --git a/package-lock.json b/package-lock.json index 09682528..b5f2871e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bybrawe/opencode-loop", - "version": "0.5.21", + "version": "0.5.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bybrawe/opencode-loop", - "version": "0.5.21", + "version": "0.5.22", "license": "MIT", "bin": { "opencode-loop": "scripts/install-node.mjs", diff --git a/package.json b/package.json index 94a448c4..0635f110 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bybrawe/opencode-loop", - "version": "0.5.21", + "version": "0.5.22", "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.", "type": "module", "main": "src/index.js", diff --git a/scripts/apply-v0522.py b/scripts/apply-v0522.py deleted file mode 100644 index 00534320..00000000 --- a/scripts/apply-v0522.py +++ /dev/null @@ -1,76 +0,0 @@ -from pathlib import Path -import json - - -def replace_once(path, old, new): - p = Path(path) - text = p.read_text() - count = text.count(old) - if count != 1: - raise SystemExit(f"{path}: expected one match, found {count}") - p.write_text(text.replace(old, new, 1)) - - -replace_once( - "src/index.js", - ''' const handled = () => { - // OpenCode server plugins cannot currently cancel the command prompt turn. - // Keep the command markdown acknowledgement intact so opencode-loop-local - // receives a valid, tool-denied message instead of an empty parts array. - return true - } -''', - ''' const handled = () => { - // OpenCode 1.18.x ignores unknown hook output fields, while proposed/newer - // hosts can honor noReply to skip the otherwise unavoidable model turn. - // Keep acknowledgement parts intact as the compatibility fallback. - if (output && typeof output === "object") output.noReply = true - return true - } -''', -) - -replace_once( - "scripts/smoke-test.mjs", - ''' assert.equal(output.parts.length, 1, "a locally handled slash command must keep a valid acknowledgement prompt") - assert.equal(output.parts[0].text, "original command body") -''', - ''' assert.equal(output.parts.length, 1, "a locally handled slash command must keep a valid acknowledgement prompt") - assert.equal(output.parts[0].text, "original command body") - assert.equal(output.noReply, true, "handled commands should request noReply for compatible OpenCode hosts") -''', -) - -package_path = Path("package.json") -package = json.loads(package_path.read_text()) -if package.get("version") != "0.5.21": - raise SystemExit(f"unexpected package version: {package.get('version')}") -package["version"] = "0.5.22" -package["devDependencies"]["@opencode-ai/plugin"] = "^1.18.15" -package_path.write_text(json.dumps(package, indent=2) + "\n") - -changelog = Path("CHANGELOG.md") -text = changelog.read_text() -entry = '''## 0.5.22 - -- Hardened compatibility with current OpenCode 1.18.15 while preserving the existing `>=1.4.0` peer range. -- Handled loop commands now also set `output.noReply = true`; OpenCode 1.18.x safely ignores the unknown field, while hosts that add the proposed `noReply` hook support can skip the acknowledgement model turn automatically. -- Added Bun runtime loading coverage so CI exercises the runtime OpenCode actually uses to load plugins, not only Node syntax/tests. -- Added explicit minimum-peer coverage against `@opencode-ai/plugin@1.4.0` and scheduled testing against the latest published OpenCode plugin package. -- Kept the v0.5.21 server-plugin acknowledgement fallback for current OpenCode versions where `command.execute.before` cannot cancel the prompt turn. - -''' -if not text.startswith("# Changelog\n\n"): - raise SystemExit("unexpected changelog header") -changelog.write_text("# Changelog\n\n" + entry + text[len("# Changelog\n\n"):]) - -readme = Path("README.md") -text = readme.read_text() -marker = "**v0.5.21 targets current OpenCode 1.18.x compatibility and safer releases.**" -if marker not in text: - raise SystemExit("README v0.5.21 marker missing") -replacement = ( - "**v0.5.22 adds forward-compatible OpenCode command handling plus Bun and peer-range compatibility CI.** " - + marker -) -readme.write_text(text.replace(marker, replacement, 1)) diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 5d651f0f..59e781cd 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -94,6 +94,7 @@ try { }, output) assert.equal(output.parts.length, 1, "a locally handled slash command must keep a valid acknowledgement prompt") assert.equal(output.parts[0].text, "original command body") + assert.equal(output.noReply, true, "handled commands should request noReply for compatible OpenCode hosts") await hooks["command.execute.before"]({ command: "loop-now", sessionID, arguments: "goal" }, { parts: [] }) assert.equal( diff --git a/src/index.js b/src/index.js index 2c4a304a..6b744506 100644 --- a/src/index.js +++ b/src/index.js @@ -2220,9 +2220,10 @@ async function handleCommand(directory, client, input, fallbackName, fallbackArg if (isLoopCommandName(name)) guardLoopOwnedUserMessage(sessionID) const handled = () => { - // OpenCode server plugins cannot currently cancel the command prompt turn. - // Keep the command markdown acknowledgement intact so opencode-loop-local - // receives a valid, tool-denied message instead of an empty parts array. + // OpenCode 1.18.x ignores unknown hook output fields, while proposed/newer + // hosts can honor noReply to skip the otherwise unavoidable model turn. + // Keep acknowledgement parts intact as the compatibility fallback. + if (output && typeof output === "object") output.noReply = true return true } From dfe7865ba659c782fc74455528b57b6ea6135ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:21:38 +0300 Subject: [PATCH 4/5] chore: remove one-shot v0.5.22 verifier --- .github/workflows/apply-v0522-pr.yml | 44 ---------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/apply-v0522-pr.yml diff --git a/.github/workflows/apply-v0522-pr.yml b/.github/workflows/apply-v0522-pr.yml deleted file mode 100644 index 86e3c2e1..00000000 --- a/.github/workflows/apply-v0522-pr.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Apply v0.5.22 on PR - -on: - pull_request: - branches: - - main - -permissions: - contents: write - -jobs: - apply: - if: github.head_ref == 'release/v0.5.22-compat-hardening' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - - uses: actions/setup-node@v6 - with: - node-version: "24" - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Apply compatibility patch - run: python scripts/apply-v0522.py - - name: Refresh lockfile - run: npm install --package-lock-only --ignore-scripts - - name: Verify Node suite and Bun loading - run: | - npm ci - npm run check - npm test - bun -e "await import('./src/index.js')" - npm pack --dry-run - - name: Commit verified code candidate - shell: bash - run: | - rm scripts/apply-v0522.py - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add src/index.js scripts/smoke-test.mjs package.json package-lock.json CHANGELOG.md README.md scripts/apply-v0522.py - git commit -m "feat: harden OpenCode compatibility for v0.5.22" - git push origin HEAD:${{ github.head_ref }} From b543f99cdaebc628fad1e5d41612a68cfe095a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:21:51 +0300 Subject: [PATCH 5/5] ci: test Bun and OpenCode peer compatibility --- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9e3eed3..4447c13e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: push: branches: - main + schedule: + - cron: "17 4 * * *" + workflow_dispatch: permissions: contents: read @@ -25,8 +28,50 @@ jobs: node-version: "24" cache: npm + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - run: npm ci + + - run: npm run check + + - run: npm test + + - name: Verify Bun can load the plugin + run: bun -e "await import('./src/index.js')" + + opencode-peer-compat: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + plugin-version: ["1.4.0", "latest"] + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: npm ci + - name: Install OpenCode plugin compatibility target + shell: bash + run: npm install --no-save --package-lock=false "@opencode-ai/plugin@${{ matrix.plugin-version }}" + + - name: Show tested OpenCode plugin version + run: node -p "require('./node_modules/@opencode-ai/plugin/package.json').version" + - run: npm run check - run: npm test + + - name: Verify Bun can load with this OpenCode plugin + run: bun -e "await import('./src/index.js')"