From 698efba99eecd09567e12a433b3ba6e45a1f6ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 18:56:31 +0300 Subject: [PATCH 01/14] chore: prepare v0.5.21 compatibility patch --- .github/workflows/apply-v0521.yml | 170 ++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/workflows/apply-v0521.yml diff --git a/.github/workflows/apply-v0521.yml b/.github/workflows/apply-v0521.yml new file mode 100644 index 00000000..f4275928 --- /dev/null +++ b/.github/workflows/apply-v0521.yml @@ -0,0 +1,170 @@ +name: Apply v0.5.21 compatibility patch + +on: + push: + branches: + - release/v0.5.21-opencode-1.18 + +permissions: + contents: write + +jobs: + apply: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: release/v0.5.21-opencode-1.18 + + - uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Apply compatibility patch + shell: bash + run: | + python - <<'PY' + from pathlib import Path + import json + import re + + 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 for {old!r}, found {count}") + p.write_text(text.replace(old, new, 1)) + + # OpenCode 1.18.x server plugins cannot cancel the command prompt turn. + # Do not create an empty message; let the tool-denied acknowledgement agent + # receive the valid prompt already present in each command markdown file. + p = Path("src/index.js") + text = p.read_text() + pattern = re.compile(r''' const handled = \(\) => \{\n if \(output && Array\.isArray\(output\.parts\)\) \{\n(?:.*\n)*? output\.parts\.length = 0\n \}\n return true\n \}\n''') + replacement = ''' const handled = () => {\n // OpenCode server plugins cannot currently cancel the command prompt turn.\n // Keep the command markdown acknowledgement intact so opencode-loop-local\n // receives a valid, tool-denied message instead of an empty parts array.\n return true\n }\n''' + text, count = pattern.subn(replacement, text, count=1) + if count != 1: + raise SystemExit(f"src/index.js: handled() block match count={count}") + text = text.replace('for (const command of ["session_compact", "session.compact"]) {', 'for (const command of ["session.compact", "session_compact"]) {', 1) + p.write_text(text) + + # Cross-platform failure fixture: no shell-significant parentheses. + replace_once("scripts/comprehensive-test.mjs", "node -e process.exit(7)", "node -e process.exitCode=7") + + # Ensure handled commands keep a valid acknowledgement prompt. + replace_once( + "scripts/comprehensive-test.mjs", + ' const beforeReports = h.reportTexts().length\n await h.command("loop-status")\n await h.commandEvent("loop-status", "", "msg_status_1")', + ' const beforeReports = h.reportTexts().length\n const statusOutput = { parts: [{ type: "text", text: "OpenCode Loop status command handled locally. Reply exactly: OK." }] }\n await h.command("loop-status", "", statusOutput)\n assert.equal(statusOutput.parts.length, 1, "handled commands must keep a valid acknowledgement prompt")\n assert.match(statusOutput.parts[0].text, /Reply exactly: OK/)\n await h.commandEvent("loop-status", "", "msg_status_1")', + ) + + # Version and development target. Keep broad peer compatibility for users. + package_path = Path("package.json") + package = json.loads(package_path.read_text()) + if package.get("version") != "0.5.20": + raise SystemExit(f"unexpected package version: {package.get('version')}") + package["version"] = "0.5.21" + package.setdefault("devDependencies", {})["@opencode-ai/plugin"] = "^1.18.15" + package_path.write_text(json.dumps(package, indent=2) + "\n") + + changelog = Path("CHANGELOG.md") + change_text = changelog.read_text() + entry = '''## 0.5.21\n\n- Verified server-plugin compatibility against OpenCode 1.18.15 and updated the development plugin dependency accordingly.\n- Stopped clearing `command.execute.before` output parts. Current OpenCode still creates a command prompt turn for server-plugin slash commands, so control commands keep the valid tool-denied `opencode-loop-local` acknowledgement instead of producing an empty message.\n- Prefer the current `session.compact` TUI command value while retaining `session_compact` and `session.summarize` as compatibility fallbacks.\n- Fixed the comprehensive preflight failure test to use a cross-platform shell-safe Node expression.\n- Added Ubuntu and Windows pull-request CI, and hardened npm publishing with full tests, tag/version verification, and `npm pack --dry-run`.\n- Retained the v0.5.20 Windows state-write retry hardening and deterministic EPERM/partial-read regressions.\n\n''' + if not change_text.startswith("# Changelog\n"): + raise SystemExit("unexpected CHANGELOG header") + changelog.write_text("# Changelog\n\n" + entry + change_text[len("# Changelog\n\n"):]) + + readme = Path("README.md") + readme_text = readme.read_text() + marker = "**v0.5.20 fixes Windows TUI state writes.**" + if marker not in readme_text: + raise SystemExit("README current-status marker missing") + 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. " + marker + readme.write_text(readme_text.replace(marker, status, 1)) + PY + + cat > .github/workflows/publish-npm.yml <<'YAML' + name: Publish to npm + + on: + push: + tags: + - "v*" + + permissions: + contents: read + id-token: write + + jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: npm + - run: npm ci + - name: Verify tag matches package version + shell: bash + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PACKAGE_VERSION="$(node -p \"require('./package.json').version\")" + test "$TAG_VERSION" = "$PACKAGE_VERSION" + - run: npm run check + - run: npm test + - run: npm pack --dry-run + - run: npm publish --access public + YAML + + cat > .github/workflows/ci.yml <<'YAML' + name: CI + + on: + pull_request: + push: + branches: + - main + + permissions: + contents: read + + jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + - run: npm ci + - run: npm run check + - run: npm test + YAML + + - name: Generate lockfile + run: npm install --package-lock-only --ignore-scripts + + - name: Verify v0.5.21 candidate + run: | + npm ci + npm run check + npm test + npm pack --dry-run + + - name: Remove one-shot workflow and commit + shell: bash + run: | + rm .github/workflows/apply-v0521.yml + 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/comprehensive-test.mjs package.json package-lock.json README.md CHANGELOG.md .github/workflows/publish-npm.yml .github/workflows/ci.yml .github/workflows/apply-v0521.yml + git commit -m "chore: prepare v0.5.21 for OpenCode 1.18" + git push origin HEAD:release/v0.5.21-opencode-1.18 From 6bbdec1cf3e061ebe08b76c49cb2113bc29d100b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 18:57:42 +0300 Subject: [PATCH 02/14] fix: make v0.5.21 patch workflow valid --- .github/workflows/apply-v0521.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/apply-v0521.yml b/.github/workflows/apply-v0521.yml index f4275928..35ad9f2c 100644 --- a/.github/workflows/apply-v0521.yml +++ b/.github/workflows/apply-v0521.yml @@ -36,9 +36,6 @@ jobs: raise SystemExit(f"{path}: expected one match for {old!r}, found {count}") p.write_text(text.replace(old, new, 1)) - # OpenCode 1.18.x server plugins cannot cancel the command prompt turn. - # Do not create an empty message; let the tool-denied acknowledgement agent - # receive the valid prompt already present in each command markdown file. p = Path("src/index.js") text = p.read_text() pattern = re.compile(r''' const handled = \(\) => \{\n if \(output && Array\.isArray\(output\.parts\)\) \{\n(?:.*\n)*? output\.parts\.length = 0\n \}\n return true\n \}\n''') @@ -46,20 +43,19 @@ jobs: text, count = pattern.subn(replacement, text, count=1) if count != 1: raise SystemExit(f"src/index.js: handled() block match count={count}") - text = text.replace('for (const command of ["session_compact", "session.compact"]) {', 'for (const command of ["session.compact", "session_compact"]) {', 1) + old_compact = 'for (const command of ["session_compact", "session.compact"]) {' + if text.count(old_compact) != 1: + raise SystemExit("src/index.js: compact command sequence changed") + text = text.replace(old_compact, 'for (const command of ["session.compact", "session_compact"]) {', 1) p.write_text(text) - # Cross-platform failure fixture: no shell-significant parentheses. replace_once("scripts/comprehensive-test.mjs", "node -e process.exit(7)", "node -e process.exitCode=7") - - # Ensure handled commands keep a valid acknowledgement prompt. replace_once( "scripts/comprehensive-test.mjs", ' const beforeReports = h.reportTexts().length\n await h.command("loop-status")\n await h.commandEvent("loop-status", "", "msg_status_1")', ' const beforeReports = h.reportTexts().length\n const statusOutput = { parts: [{ type: "text", text: "OpenCode Loop status command handled locally. Reply exactly: OK." }] }\n await h.command("loop-status", "", statusOutput)\n assert.equal(statusOutput.parts.length, 1, "handled commands must keep a valid acknowledgement prompt")\n assert.match(statusOutput.parts[0].text, /Reply exactly: OK/)\n await h.commandEvent("loop-status", "", "msg_status_1")', ) - # Version and development target. Keep broad peer compatibility for users. package_path = Path("package.json") package = json.loads(package_path.read_text()) if package.get("version") != "0.5.20": @@ -119,7 +115,8 @@ jobs: - run: npm publish --access public YAML - cat > .github/workflows/ci.yml <<'YAML' + MATRIX_OS='$'"{{ matrix.os }}" + cat > .github/workflows/ci.yml < Date: Sat, 8 Aug 2026 18:58:34 +0300 Subject: [PATCH 03/14] chore: trigger v0.5.21 validation --- .v0521-trigger | 1 + 1 file changed, 1 insertion(+) create mode 100644 .v0521-trigger diff --git a/.v0521-trigger b/.v0521-trigger new file mode 100644 index 00000000..5c33b151 --- /dev/null +++ b/.v0521-trigger @@ -0,0 +1 @@ +trigger From 1692fcfc19616ef9eb2a3a6cbeea8ae525519906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 18:59:20 +0300 Subject: [PATCH 04/14] chore: add temporary v0.5.21 patch script --- scripts/apply-v0521.py | 143 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 scripts/apply-v0521.py diff --git a/scripts/apply-v0521.py b/scripts/apply-v0521.py new file mode 100644 index 00000000..121bdec8 --- /dev/null +++ b/scripts/apply-v0521.py @@ -0,0 +1,143 @@ +from pathlib import Path +import json +import re + + +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 for {old!r}, found {count}") + p.write_text(text.replace(old, new, 1)) + + +# OpenCode 1.18.x server plugins cannot cancel the command prompt turn. +# Keep a valid prompt for the locked-down local acknowledgement agent instead +# of clearing output.parts and creating an empty command message. +p = Path("src/index.js") +text = p.read_text() +pattern = re.compile( + r''' const handled = \(\) => \{\n if \(output && Array\.isArray\(output\.parts\)\) \{\n(?:.*\n)*? output\.parts\.length = 0\n \}\n return true\n \}\n''' +) +replacement = ''' const handled = () => {\n // OpenCode server plugins cannot currently cancel the command prompt turn.\n // Keep the command markdown acknowledgement intact so opencode-loop-local\n // receives a valid, tool-denied message instead of an empty parts array.\n return true\n }\n''' +text, count = pattern.subn(replacement, text, count=1) +if count != 1: + raise SystemExit(f"src/index.js: handled() block match count={count}") + +old_compact = 'for (const command of ["session_compact", "session.compact"]) {' +if text.count(old_compact) != 1: + raise SystemExit("src/index.js: compact command sequence changed") +text = text.replace(old_compact, 'for (const command of ["session.compact", "session_compact"]) {', 1) +p.write_text(text) + +# Cross-platform failure fixture: no shell-significant parentheses. +replace_once("scripts/comprehensive-test.mjs", "node -e process.exit(7)", "node -e process.exitCode=7") + +# Current OpenCode still runs the command prompt after command.execute.before. +# Verify we retain a valid acknowledgement part for opencode-loop-local. +replace_once( + "scripts/comprehensive-test.mjs", + ' const beforeReports = h.reportTexts().length\n await h.command("loop-status")\n await h.commandEvent("loop-status", "", "msg_status_1")', + ' const beforeReports = h.reportTexts().length\n const statusOutput = { parts: [{ type: "text", text: "OpenCode Loop status command handled locally. Reply exactly: OK." }] }\n await h.command("loop-status", "", statusOutput)\n assert.equal(statusOutput.parts.length, 1, "handled commands must keep a valid acknowledgement prompt")\n assert.match(statusOutput.parts[0].text, /Reply exactly: OK/)\n await h.commandEvent("loop-status", "", "msg_status_1")', +) + +package_path = Path("package.json") +package = json.loads(package_path.read_text()) +if package.get("version") != "0.5.20": + raise SystemExit(f"unexpected package version: {package.get('version')}") +package["version"] = "0.5.21" +package.setdefault("devDependencies", {})["@opencode-ai/plugin"] = "^1.18.15" +package_path.write_text(json.dumps(package, indent=2) + "\n") + +changelog = Path("CHANGELOG.md") +change_text = changelog.read_text() +entry = """## 0.5.21 + +- Verified server-plugin compatibility against OpenCode 1.18.15 and updated the development plugin dependency accordingly. +- Stopped clearing `command.execute.before` output parts. Current OpenCode still creates a command prompt turn for server-plugin slash commands, so control commands keep the valid tool-denied `opencode-loop-local` acknowledgement instead of producing an empty message. +- Prefer the current `session.compact` TUI command value while retaining `session_compact` and `session.summarize` as compatibility fallbacks. +- Fixed the comprehensive preflight failure test to use a cross-platform shell-safe Node expression. +- Added Ubuntu and Windows pull-request CI, and hardened npm publishing with full tests, tag/version verification, and `npm pack --dry-run`. +- Retained the v0.5.20 Windows state-write retry hardening and deterministic EPERM/partial-read regressions. + +""" +if not change_text.startswith("# Changelog\n"): + raise SystemExit("unexpected CHANGELOG header") +changelog.write_text("# Changelog\n\n" + entry + change_text[len("# Changelog\n\n"):]) + +readme = Path("README.md") +readme_text = readme.read_text() +marker = "**v0.5.20 fixes Windows TUI state writes.**" +if marker not in readme_text: + raise SystemExit("README current-status marker missing") +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. " + + marker +) +readme.write_text(readme_text.replace(marker, status, 1)) + +Path(".github/workflows/publish-npm.yml").write_text("""name: Publish to npm + +on: + push: + tags: + - "v*" + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: npm + - run: npm ci + - name: Verify tag matches package version + shell: bash + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PACKAGE_VERSION="$(node -p \"require('./package.json').version\")" + test "$TAG_VERSION" = "$PACKAGE_VERSION" + - run: npm run check + - run: npm test + - run: npm pack --dry-run + - run: npm publish --access public +""") + +Path(".github/workflows/ci.yml").write_text("""name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + - run: npm ci + - run: npm run check + - run: npm test +""") From 971567c473e9128323920a737e51231916c771db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 18:59:31 +0300 Subject: [PATCH 05/14] chore: validate and apply v0.5.21 on PR --- .github/workflows/apply-v0521-pr.yml | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/apply-v0521-pr.yml diff --git a/.github/workflows/apply-v0521-pr.yml b/.github/workflows/apply-v0521-pr.yml new file mode 100644 index 00000000..14cbccf4 --- /dev/null +++ b/.github/workflows/apply-v0521-pr.yml @@ -0,0 +1,40 @@ +name: Apply v0.5.21 on PR + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + apply: + if: github.head_ref == 'release/v0.5.21-opencode-1.18' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + - uses: actions/setup-node@v6 + with: + node-version: "24" + - name: Apply patch + run: python scripts/apply-v0521.py + - name: Generate lockfile + run: npm install --package-lock-only --ignore-scripts + - name: Verify candidate + run: | + npm ci + npm run check + npm test + npm pack --dry-run + - name: Commit verified candidate + shell: bash + run: | + rm -f .github/workflows/apply-v0521.yml .github/workflows/apply-v0521-pr.yml .v0521-trigger scripts/apply-v0521.py + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "chore: prepare v0.5.21 for OpenCode 1.18" + git push origin HEAD:${{ github.head_ref }} From 932ad3b045d11a9a03c855c8f5805ecff42b2fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:00:45 +0300 Subject: [PATCH 06/14] test: align smoke test with OpenCode command behavior --- scripts/apply-v0521.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/apply-v0521.py b/scripts/apply-v0521.py index 121bdec8..7aa86f6d 100644 --- a/scripts/apply-v0521.py +++ b/scripts/apply-v0521.py @@ -35,7 +35,12 @@ def replace_once(path, old, new): replace_once("scripts/comprehensive-test.mjs", "node -e process.exit(7)", "node -e process.exitCode=7") # Current OpenCode still runs the command prompt after command.execute.before. -# Verify we retain a valid acknowledgement part for opencode-loop-local. +# Verify both smoke and comprehensive tests retain valid acknowledgement parts. +replace_once( + "scripts/smoke-test.mjs", + ' assert.equal(output.parts.length, 0, "a locally handled slash command must not start a placeholder model turn")', + ' assert.equal(output.parts.length, 1, "a locally handled slash command must keep a valid acknowledgement prompt")\n assert.equal(output.parts[0].text, "original command body")', +) replace_once( "scripts/comprehensive-test.mjs", ' const beforeReports = h.reportTexts().length\n await h.command("loop-status")\n await h.commandEvent("loop-status", "", "msg_status_1")', From 5b40947287144906c3c816a5d355e5b66fc1d730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:01:43 +0300 Subject: [PATCH 07/14] fix: push verified code separately from workflows --- .github/workflows/apply-v0521-pr.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/apply-v0521-pr.yml b/.github/workflows/apply-v0521-pr.yml index 14cbccf4..ce625b3b 100644 --- a/.github/workflows/apply-v0521-pr.yml +++ b/.github/workflows/apply-v0521-pr.yml @@ -29,12 +29,11 @@ jobs: npm run check npm test npm pack --dry-run - - name: Commit verified candidate + - name: Commit verified code candidate shell: bash run: | - rm -f .github/workflows/apply-v0521.yml .github/workflows/apply-v0521-pr.yml .v0521-trigger scripts/apply-v0521.py git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A + git add src/index.js scripts/smoke-test.mjs scripts/comprehensive-test.mjs package.json package-lock.json README.md CHANGELOG.md git commit -m "chore: prepare v0.5.21 for OpenCode 1.18" git push origin HEAD:${{ github.head_ref }} From b8d48c967a0e323db1dac204825ef565390516e8 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:02:23 +0000 Subject: [PATCH 08/14] chore: prepare v0.5.21 for OpenCode 1.18 --- CHANGELOG.md | 9 + README.md | 2 +- package-lock.json | 444 +++++++++++++++++++++++++++++++++ package.json | 4 +- scripts/comprehensive-test.mjs | 7 +- scripts/smoke-test.mjs | 3 +- src/index.js | 11 +- 7 files changed, 467 insertions(+), 13 deletions(-) create mode 100644 package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e86bb77..b5a56ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.5.21 + +- Verified server-plugin compatibility against OpenCode 1.18.15 and updated the development plugin dependency accordingly. +- Stopped clearing `command.execute.before` output parts. Current OpenCode still creates a command prompt turn for server-plugin slash commands, so control commands keep the valid tool-denied `opencode-loop-local` acknowledgement instead of producing an empty message. +- Prefer the current `session.compact` TUI command value while retaining `session_compact` and `session.summarize` as compatibility fallbacks. +- Fixed the comprehensive preflight failure test to use a cross-platform shell-safe Node expression. +- Added Ubuntu and Windows pull-request CI, and hardened npm publishing with full tests, tag/version verification, and `npm pack --dry-run`. +- Retained the v0.5.20 Windows state-write retry hardening and deterministic EPERM/partial-read regressions. + ## 0.5.20 - Fixed Windows TUI loop state writes failing with `EPERM` / `EEXIST` when renaming a project-local `*.tmp` over an existing session state file. Heartbeat and due-timer updates no longer drop jobs when antivirus, IDE indexers, or OpenCode snapshots briefly lock the destination. diff --git a/README.md b/README.md index 8f8c717c..bba6783a 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.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.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 new file mode 100644 index 00000000..09682528 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,444 @@ +{ + "name": "@bybrawe/opencode-loop", + "version": "0.5.21", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@bybrawe/opencode-loop", + "version": "0.5.21", + "license": "MIT", + "bin": { + "opencode-loop": "scripts/install-node.mjs", + "opencode-loopd": "scripts/loopd.mjs" + }, + "devDependencies": { + "@opencode-ai/plugin": "^1.18.15" + }, + "peerDependencies": { + "@opencode-ai/plugin": ">=1.4.0" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.18.15", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.18.15.tgz", + "integrity": "sha512-AY10RtFbzLkf951dbLkSGJdBeddeS6ojbjvqCpWnINedqlj2cK/GF91xWjWnlHpqQZ4GABLPCuoSJx8mGmKvCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@opencode-ai/sdk": "1.18.15", + "effect": "4.0.0-beta.83", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.4.5", + "@opentui/keymap": ">=0.4.5", + "@opentui/solid": ">=0.4.5" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.18.15", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.15.tgz", + "integrity": "sha512-8sfo9nGiVwesAZW9Wqkvynyn7w4wYaHx1O9qOHpYL65+Bs2XUpHP3kBbZe58gjQydFgk6I74kUF7sqCiPu2arQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.83", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.83.tgz", + "integrity": "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.8.0", + "find-my-way-ts": "^0.1.6", + "ini": "^7.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^2.0.1", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^14.0.0", + "yaml": "^2.9.0" + } + }, + "node_modules/fast-check": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.9.0.tgz", + "integrity": "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ini": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-7.0.0.tgz", + "integrity": "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/msgpackr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.5.tgz", + "integrity": "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multipasta": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.8.tgz", + "integrity": "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/toml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz", + "integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index fbcde98e..94a448c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bybrawe/opencode-loop", - "version": "0.5.20", + "version": "0.5.21", "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", @@ -57,7 +57,7 @@ "@opencode-ai/plugin": ">=1.4.0" }, "devDependencies": { - "@opencode-ai/plugin": "^1.4.0" + "@opencode-ai/plugin": "^1.18.15" }, "bin": { "opencode-loop": "scripts/install-node.mjs", diff --git a/scripts/comprehensive-test.mjs b/scripts/comprehensive-test.mjs index 34d2b548..4620cb03 100644 --- a/scripts/comprehensive-test.mjs +++ b/scripts/comprehensive-test.mjs @@ -243,7 +243,10 @@ async function testLifecycleAndCommandDedupe() { assert.equal(state.jobs.find((item) => item.name === "same").paused, false) const beforeReports = h.reportTexts().length - await h.command("loop-status") + const statusOutput = { parts: [{ type: "text", text: "OpenCode Loop status command handled locally. Reply exactly: OK." }] } + await h.command("loop-status", "", statusOutput) + assert.equal(statusOutput.parts.length, 1, "handled commands must keep a valid acknowledgement prompt") + assert.match(statusOutput.parts[0].text, /Reply exactly: OK/) await h.commandEvent("loop-status", "", "msg_status_1") assert.equal(h.reportTexts().length, beforeReports + 1, "command.executed must not duplicate the before hook") await h.command("loop-status") @@ -384,7 +387,7 @@ async function testStopsPreflightAndGoalLifecycle() { h = await createHarness() try { - await h.command("loop", "5m --no-now --preflight \"node -e process.exit(7)\" continue") + await h.command("loop", "5m --no-now --preflight \"node -e process.exitCode=7\" continue") await h.command("loop-now") const state = await h.readState() assert.equal(state.jobs[0].paused, true) diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index edc4444c..5d651f0f 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -92,7 +92,8 @@ try { sessionID, arguments: "Create proof.txt and verify it --max-turns 3", }, output) - assert.equal(output.parts.length, 0, "a locally handled slash command must not start a placeholder model turn") + 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") 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 dd868cf5..2c4a304a 100644 --- a/src/index.js +++ b/src/index.js @@ -535,7 +535,7 @@ async function compactSession(client, sessionID) { // current builds, while some older docs/examples mention the event value // (session.compact). Try the alias first, then the event value, then the // session summarize endpoint as a last resort. - for (const command of ["session_compact", "session.compact"]) { + for (const command of ["session.compact", "session_compact"]) { try { await executeTuiCommand(client, command) return true @@ -2220,12 +2220,9 @@ async function handleCommand(directory, client, input, fallbackName, fallbackArg if (isLoopCommandName(name)) guardLoopOwnedUserMessage(sessionID) const handled = () => { - if (output && Array.isArray(output.parts)) { - // The command has already been completed through toasts/noReply prompts - // and local state. Leaving a placeholder prompt starts an unnecessary - // model turn; weaker agents may even call tools or spawn subagents. - output.parts.length = 0 - } + // 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 } From e07b4f944b2bbc2ede2796e12f59b8f3bc424032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:03:00 +0300 Subject: [PATCH 09/14] chore: remove temporary PR patch workflow --- .github/workflows/apply-v0521-pr.yml | 39 ---------------------------- 1 file changed, 39 deletions(-) delete mode 100644 .github/workflows/apply-v0521-pr.yml diff --git a/.github/workflows/apply-v0521-pr.yml b/.github/workflows/apply-v0521-pr.yml deleted file mode 100644 index ce625b3b..00000000 --- a/.github/workflows/apply-v0521-pr.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Apply v0.5.21 on PR - -on: - pull_request: - branches: - - main - -permissions: - contents: write - -jobs: - apply: - if: github.head_ref == 'release/v0.5.21-opencode-1.18' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - - uses: actions/setup-node@v6 - with: - node-version: "24" - - name: Apply patch - run: python scripts/apply-v0521.py - - name: Generate lockfile - run: npm install --package-lock-only --ignore-scripts - - name: Verify candidate - run: | - npm ci - npm run check - npm test - npm pack --dry-run - - name: Commit verified code candidate - shell: bash - run: | - 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 scripts/comprehensive-test.mjs package.json package-lock.json README.md CHANGELOG.md - git commit -m "chore: prepare v0.5.21 for OpenCode 1.18" - git push origin HEAD:${{ github.head_ref }} From b68d3a9275985008d0ad5979f2a6ed650d84a832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:03:06 +0300 Subject: [PATCH 10/14] chore: remove temporary patch workflow --- .github/workflows/apply-v0521.yml | 167 ------------------------------ 1 file changed, 167 deletions(-) delete mode 100644 .github/workflows/apply-v0521.yml diff --git a/.github/workflows/apply-v0521.yml b/.github/workflows/apply-v0521.yml deleted file mode 100644 index 35ad9f2c..00000000 --- a/.github/workflows/apply-v0521.yml +++ /dev/null @@ -1,167 +0,0 @@ -name: Apply v0.5.21 compatibility patch - -on: - push: - branches: - - release/v0.5.21-opencode-1.18 - -permissions: - contents: write - -jobs: - apply: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: release/v0.5.21-opencode-1.18 - - - uses: actions/setup-node@v6 - with: - node-version: "24" - - - name: Apply compatibility patch - shell: bash - run: | - python - <<'PY' - from pathlib import Path - import json - import re - - 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 for {old!r}, found {count}") - p.write_text(text.replace(old, new, 1)) - - p = Path("src/index.js") - text = p.read_text() - pattern = re.compile(r''' const handled = \(\) => \{\n if \(output && Array\.isArray\(output\.parts\)\) \{\n(?:.*\n)*? output\.parts\.length = 0\n \}\n return true\n \}\n''') - replacement = ''' const handled = () => {\n // OpenCode server plugins cannot currently cancel the command prompt turn.\n // Keep the command markdown acknowledgement intact so opencode-loop-local\n // receives a valid, tool-denied message instead of an empty parts array.\n return true\n }\n''' - text, count = pattern.subn(replacement, text, count=1) - if count != 1: - raise SystemExit(f"src/index.js: handled() block match count={count}") - old_compact = 'for (const command of ["session_compact", "session.compact"]) {' - if text.count(old_compact) != 1: - raise SystemExit("src/index.js: compact command sequence changed") - text = text.replace(old_compact, 'for (const command of ["session.compact", "session_compact"]) {', 1) - p.write_text(text) - - replace_once("scripts/comprehensive-test.mjs", "node -e process.exit(7)", "node -e process.exitCode=7") - replace_once( - "scripts/comprehensive-test.mjs", - ' const beforeReports = h.reportTexts().length\n await h.command("loop-status")\n await h.commandEvent("loop-status", "", "msg_status_1")', - ' const beforeReports = h.reportTexts().length\n const statusOutput = { parts: [{ type: "text", text: "OpenCode Loop status command handled locally. Reply exactly: OK." }] }\n await h.command("loop-status", "", statusOutput)\n assert.equal(statusOutput.parts.length, 1, "handled commands must keep a valid acknowledgement prompt")\n assert.match(statusOutput.parts[0].text, /Reply exactly: OK/)\n await h.commandEvent("loop-status", "", "msg_status_1")', - ) - - package_path = Path("package.json") - package = json.loads(package_path.read_text()) - if package.get("version") != "0.5.20": - raise SystemExit(f"unexpected package version: {package.get('version')}") - package["version"] = "0.5.21" - package.setdefault("devDependencies", {})["@opencode-ai/plugin"] = "^1.18.15" - package_path.write_text(json.dumps(package, indent=2) + "\n") - - changelog = Path("CHANGELOG.md") - change_text = changelog.read_text() - entry = '''## 0.5.21\n\n- Verified server-plugin compatibility against OpenCode 1.18.15 and updated the development plugin dependency accordingly.\n- Stopped clearing `command.execute.before` output parts. Current OpenCode still creates a command prompt turn for server-plugin slash commands, so control commands keep the valid tool-denied `opencode-loop-local` acknowledgement instead of producing an empty message.\n- Prefer the current `session.compact` TUI command value while retaining `session_compact` and `session.summarize` as compatibility fallbacks.\n- Fixed the comprehensive preflight failure test to use a cross-platform shell-safe Node expression.\n- Added Ubuntu and Windows pull-request CI, and hardened npm publishing with full tests, tag/version verification, and `npm pack --dry-run`.\n- Retained the v0.5.20 Windows state-write retry hardening and deterministic EPERM/partial-read regressions.\n\n''' - if not change_text.startswith("# Changelog\n"): - raise SystemExit("unexpected CHANGELOG header") - changelog.write_text("# Changelog\n\n" + entry + change_text[len("# Changelog\n\n"):]) - - readme = Path("README.md") - readme_text = readme.read_text() - marker = "**v0.5.20 fixes Windows TUI state writes.**" - if marker not in readme_text: - raise SystemExit("README current-status marker missing") - 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. " + marker - readme.write_text(readme_text.replace(marker, status, 1)) - PY - - cat > .github/workflows/publish-npm.yml <<'YAML' - name: Publish to npm - - on: - push: - tags: - - "v*" - - permissions: - contents: read - id-token: write - - jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: "24" - registry-url: "https://registry.npmjs.org" - cache: npm - - run: npm ci - - name: Verify tag matches package version - shell: bash - run: | - TAG_VERSION="${GITHUB_REF_NAME#v}" - PACKAGE_VERSION="$(node -p \"require('./package.json').version\")" - test "$TAG_VERSION" = "$PACKAGE_VERSION" - - run: npm run check - - run: npm test - - run: npm pack --dry-run - - run: npm publish --access public - YAML - - MATRIX_OS='$'"{{ matrix.os }}" - cat > .github/workflows/ci.yml < Date: Sat, 8 Aug 2026 19:03:12 +0300 Subject: [PATCH 11/14] chore: remove temporary patch script --- scripts/apply-v0521.py | 148 ----------------------------------------- 1 file changed, 148 deletions(-) delete mode 100644 scripts/apply-v0521.py diff --git a/scripts/apply-v0521.py b/scripts/apply-v0521.py deleted file mode 100644 index 7aa86f6d..00000000 --- a/scripts/apply-v0521.py +++ /dev/null @@ -1,148 +0,0 @@ -from pathlib import Path -import json -import re - - -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 for {old!r}, found {count}") - p.write_text(text.replace(old, new, 1)) - - -# OpenCode 1.18.x server plugins cannot cancel the command prompt turn. -# Keep a valid prompt for the locked-down local acknowledgement agent instead -# of clearing output.parts and creating an empty command message. -p = Path("src/index.js") -text = p.read_text() -pattern = re.compile( - r''' const handled = \(\) => \{\n if \(output && Array\.isArray\(output\.parts\)\) \{\n(?:.*\n)*? output\.parts\.length = 0\n \}\n return true\n \}\n''' -) -replacement = ''' const handled = () => {\n // OpenCode server plugins cannot currently cancel the command prompt turn.\n // Keep the command markdown acknowledgement intact so opencode-loop-local\n // receives a valid, tool-denied message instead of an empty parts array.\n return true\n }\n''' -text, count = pattern.subn(replacement, text, count=1) -if count != 1: - raise SystemExit(f"src/index.js: handled() block match count={count}") - -old_compact = 'for (const command of ["session_compact", "session.compact"]) {' -if text.count(old_compact) != 1: - raise SystemExit("src/index.js: compact command sequence changed") -text = text.replace(old_compact, 'for (const command of ["session.compact", "session_compact"]) {', 1) -p.write_text(text) - -# Cross-platform failure fixture: no shell-significant parentheses. -replace_once("scripts/comprehensive-test.mjs", "node -e process.exit(7)", "node -e process.exitCode=7") - -# Current OpenCode still runs the command prompt after command.execute.before. -# Verify both smoke and comprehensive tests retain valid acknowledgement parts. -replace_once( - "scripts/smoke-test.mjs", - ' assert.equal(output.parts.length, 0, "a locally handled slash command must not start a placeholder model turn")', - ' assert.equal(output.parts.length, 1, "a locally handled slash command must keep a valid acknowledgement prompt")\n assert.equal(output.parts[0].text, "original command body")', -) -replace_once( - "scripts/comprehensive-test.mjs", - ' const beforeReports = h.reportTexts().length\n await h.command("loop-status")\n await h.commandEvent("loop-status", "", "msg_status_1")', - ' const beforeReports = h.reportTexts().length\n const statusOutput = { parts: [{ type: "text", text: "OpenCode Loop status command handled locally. Reply exactly: OK." }] }\n await h.command("loop-status", "", statusOutput)\n assert.equal(statusOutput.parts.length, 1, "handled commands must keep a valid acknowledgement prompt")\n assert.match(statusOutput.parts[0].text, /Reply exactly: OK/)\n await h.commandEvent("loop-status", "", "msg_status_1")', -) - -package_path = Path("package.json") -package = json.loads(package_path.read_text()) -if package.get("version") != "0.5.20": - raise SystemExit(f"unexpected package version: {package.get('version')}") -package["version"] = "0.5.21" -package.setdefault("devDependencies", {})["@opencode-ai/plugin"] = "^1.18.15" -package_path.write_text(json.dumps(package, indent=2) + "\n") - -changelog = Path("CHANGELOG.md") -change_text = changelog.read_text() -entry = """## 0.5.21 - -- Verified server-plugin compatibility against OpenCode 1.18.15 and updated the development plugin dependency accordingly. -- Stopped clearing `command.execute.before` output parts. Current OpenCode still creates a command prompt turn for server-plugin slash commands, so control commands keep the valid tool-denied `opencode-loop-local` acknowledgement instead of producing an empty message. -- Prefer the current `session.compact` TUI command value while retaining `session_compact` and `session.summarize` as compatibility fallbacks. -- Fixed the comprehensive preflight failure test to use a cross-platform shell-safe Node expression. -- Added Ubuntu and Windows pull-request CI, and hardened npm publishing with full tests, tag/version verification, and `npm pack --dry-run`. -- Retained the v0.5.20 Windows state-write retry hardening and deterministic EPERM/partial-read regressions. - -""" -if not change_text.startswith("# Changelog\n"): - raise SystemExit("unexpected CHANGELOG header") -changelog.write_text("# Changelog\n\n" + entry + change_text[len("# Changelog\n\n"):]) - -readme = Path("README.md") -readme_text = readme.read_text() -marker = "**v0.5.20 fixes Windows TUI state writes.**" -if marker not in readme_text: - raise SystemExit("README current-status marker missing") -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. " - + marker -) -readme.write_text(readme_text.replace(marker, status, 1)) - -Path(".github/workflows/publish-npm.yml").write_text("""name: Publish to npm - -on: - push: - tags: - - "v*" - -permissions: - contents: read - id-token: write - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: "24" - registry-url: "https://registry.npmjs.org" - cache: npm - - run: npm ci - - name: Verify tag matches package version - shell: bash - run: | - TAG_VERSION="${GITHUB_REF_NAME#v}" - PACKAGE_VERSION="$(node -p \"require('./package.json').version\")" - test "$TAG_VERSION" = "$PACKAGE_VERSION" - - run: npm run check - - run: npm test - - run: npm pack --dry-run - - run: npm publish --access public -""") - -Path(".github/workflows/ci.yml").write_text("""name: CI - -on: - pull_request: - push: - branches: - - main - -permissions: - contents: read - -jobs: - test: - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: "24" - cache: npm - - run: npm ci - - run: npm run check - - run: npm test -""") From 52874ecdf72426ae9e4b0a408e79057324311497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:03:23 +0300 Subject: [PATCH 12/14] chore: remove temporary release trigger --- .v0521-trigger | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .v0521-trigger diff --git a/.v0521-trigger b/.v0521-trigger deleted file mode 100644 index 5c33b151..00000000 --- a/.v0521-trigger +++ /dev/null @@ -1 +0,0 @@ -trigger From 4d69fdf16a84a6bd26d3331f72c45224d87ad7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:03:39 +0300 Subject: [PATCH 13/14] ci: harden npm release verification --- .github/workflows/publish-npm.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 8c4fc779..74cefb55 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -20,9 +20,21 @@ jobs: with: node-version: "24" registry-url: "https://registry.npmjs.org" + cache: npm - - run: npm ci || npm install + - run: npm ci - - run: npm run check --if-present + - name: Verify tag matches package version + shell: bash + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PACKAGE_VERSION="$(node -p "require('./package.json').version")" + test "$TAG_VERSION" = "$PACKAGE_VERSION" - - run: npm publish --access public \ No newline at end of file + - run: npm run check + + - run: npm test + + - run: npm pack --dry-run + + - run: npm publish --access public From e5f3a13b4ba4477be8ecafdabb2596c4bd4c98b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Sat, 8 Aug 2026 19:03:46 +0300 Subject: [PATCH 14/14] ci: test pull requests on Ubuntu and Windows --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..a9e3eed3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + + - run: npm ci + + - run: npm run check + + - run: npm test