Skip to content

feat(plugin): OpenCode V2 support and reproducible bundling (#38, #43) - #44

Merged
antongulin merged 5 commits into
mainfrom
fix/opencode-v2-delivery
Oct 1, 2026
Merged

antongulin merged 5 commits into
mainfrom
fix/opencode-v2-delivery

Conversation

@antongulin

Copy link
Copy Markdown
Owner

Summary

Adds OpenCode V2 plugin support and fixes the reproducible-bundling problem. Two linked issues:

What changed

V2 plugin entrypoint (#38)

  • plugin/skill-creator.ts now exports a default definition with id + setup() alongside the existing V1 server(). One shared tool registry (zod shapes) drives both entrypoints; V2 returns { content } results and derives JSON Schema via zod.toJSONSchema, registering the full public skill_* tool surface (14 tools).
  • V2 setup() installs a ctx.skill.list conflict-guard enumerator so the eval guard works on V2 instead of silently no-oping on the removed opencode debug skill. It resolves the eval/project root from ctx.location.directory and returns a cleanup function that stops the review servers it started.
  • V2 state is per instance: two locations in one process keep independent enumerators, project roots and review servers; one instance's cleanup never closes another's.

Eval conflict guard

  • plugin/lib/run-eval.ts: the guard takes an injectable enumerator and aborts with an actionable error when enumeration is unavailable, rather than warn-and-continue into a contaminated eval.

Build correctness (#43)

  • plugin/scripts/build.mjs: --external:<name> → the working --external=<name> form for both SDK packages; the bundle now externalizes them and inlines no node_modules. Dist drops ~0.55 MB → ~112 KB.
  • plugin/package.json: required peer @opencode-ai/plugin >= 1.18.29, optional peer @opencode/plugin ^2.0.0, and a tracked package-lock.json for reproducible installs.
  • plugin/bin/opencode-skill-creator.js: installer writes the V2 plugins key while preserving any legacy plugin array and object entries.

Safety / hygiene

  • plugin/lib/review-server.ts: no longer SIGTERMs whatever holds the requested port; binds without killing other processes and fails loudly on EADDRINUSE.
  • skill_serve_review gains an explicit openBrowser argument (default true) with an OPENCODE_SKILL_CREATOR_OPEN_BROWSER=0|false process override, so automated callers/tests never open the user's browser.

Workflows / tests / docs

  • .github/workflows/publish.yml: npm install → npm ci; stage the lockfile with the automatic version bump.
  • .github/workflows/ci.yml (new): PR/main CI on Node 22 + Bun — npm ci, npm test, npm run test:ts, npm run build, npm pack.
  • New/expanded tests: real V2 runtime load + tool execution from the packed tarball, real V1 >= 1.18.29 floor, D-02 installed-skill conflict + clean state, eval subprocess harness, review preflight, busy-port preservation, per-instance isolation, and pack-manifest checks.
  • README.md / plugin/README.md: compatibility table, plugins key, and the V1/V2 floor.

Testing

  • npm test — 46 pass / 0 fail (with a provisioned V1 floor binary; 40 + 1 skip without).
  • npm run test:ts — 81 pass / 0 fail.
  • npm run build — dist imports only @opencode-ai/plugin (external); 0 inlined node_modules.
  • Real OpenCode v2.0.19 (isolated HOME/XDG/process): plugin loads (no PluginModule.LoadError), 14 skill_* tools registered, skill_parse/skill_validate execute from the packed tarball.
  • Real V1 floor opencode-ai@1.18.29: object entrypoint loads and lists the bundled skill.
  • Live bounded eval (2 authenticated queries, deepseek-v4.1-flash): positive trigger 1 / negative trigger 0, summary{total:2, passed:2, run_errors:0}.
  • PR CI runs the same suites on Node 22 + Bun.

Limitations

  • Windows/Linux runtime behavior is covered by the existing cross-platform branches and CI only; native QA was macOS.
  • The V2 ctx.skill.list conflict/clean-state path is proven end-to-end in the real runtime; the enumerator unit test is supporting wiring coverage.

Credit

Builds on the V2 port proposed by @sogeisetsu in the fork (sogeisetsu/feat/opencode-v2-support), adopted with corrections and attribution. Thanks for the detailed #38 report.

Co-authored-by: sogeisetsu 47493432+sogeisetsu@users.noreply.github.com

opencode-agent and others added 3 commits September 30, 2026 15:24
OpenCode V2 reads a default export with an `id` and `setup()`; the published
package only exported a V1 function, which produced
`PluginModule.LoadError` on load (issue #38). The build also inlined
`@opencode-ai/plugin`, zod, and the SDK into a 0.55 MB bundle because the
`--external:<name>` flag is a bun no-op (issue #43).

- `skill-creator.ts`: one shared tool registry (zod shapes) drives both the
  V1 `server()` hooks and the V2 `id`/`setup()` entrypoint; V2 derives JSON
  Schema via `zod.toJSONSchema`, returns `{ content }`, and returns a cleanup
  function that stops active review servers. `setup()` overrides the eval
  conflict-guard enumerator with `ctx.skill.list`, so the guard now works on
  V2 instead of silently no-oping on the removed `opencode debug skill`.
- `lib/run-eval.ts`: the conflict guard takes an injectable enumerator and
  aborts with an actionable error when enumeration is unavailable, rather than
  warn-and-continue into a contaminated eval.
- `lib/review-server.ts`: stop SIGTERMing whatever holds the requested port;
  bind without killing other processes and fail loudly on EADDRINUSE.
- `scripts/build.mjs`: use the working `--external=<name>` form for both SDK
  packages; the bundle now externalizes both and inlines no node_modules.
- `bin/opencode-skill-creator.js`: migrate the installer to the V2 `plugins`
  key while preserving any legacy `plugin` array and object-form entries.
- `package.json`: keep `@opencode-ai/plugin` as a required peer at the V2
  documented floor (>= 1.18.29) and add `@opencode/plugin` as an optional
  peer; track `package-lock.json` for reproducible installs.
- Workflows: `npm install` -> `npm ci`, stage the lockfile with the automatic
  version bump, and add a PR CI job using the existing npm + Bun suites.
- Tests: real V2 runtime load + tool execution from the packed tarball, real
  V1 >= 1.18.29 floor load, installer key migration, eval subprocess harness,
  conflict-guard injection, review preflight, port-preservation, and pack
  manifest checks.

Co-authored-by: sogeisetsu <47493432+sogeisetsu@users.noreply.github.com>
…03 gates

Stage 1 correction 1 (still no PR). Addresses lead inspection findings.

Changed-state correctness:
- Replace the module-global `activeSkillEnumerator` and `activeServers` map with
  a per-instance object created in each V2 `setup()`. Two locations in one
  process now keep independent enumerators, project roots and review servers,
  and one instance's cleanup never closes another's servers. V1 keeps its
  process-wide instance (cwd root, `opencode debug skill`) unchanged.
- V2 resolves the eval project root from `ctx.location.directory` (falling back
  through `findProjectRoot`) instead of `process.cwd`, and `setup()` fails
  loudly when the location directory is missing.
- `createV2SkillEnumerator` now scopes `ctx.skill.list({ location })` to the
  instance's project root.

Test isolation (root cause of the stray global skill):
- Earlier `setup()`/`server()` unit tests ran without isolating HOME/XDG, so the
  process-wide bundled-skill install wrote `~/.config/opencode/skills`. Wrap all
  startup calls in `withPrivateHome`, disable the registry auto-update check,
  and add a regression asserting the real config is byte-for-byte unchanged.

Tests:
- Real V2 runtime: add D-02 conflict end-to-end (installed-skill collision makes
  `skill_eval` abort) and a clean-state eval against the isolated synthetic
  skill with a controlled fake child.
- runEval harness: feed the captured real v2 `tool_use` event through the actual
  parser; cover chunk boundaries, final buffer without newline, agent/model argv,
  and cwd/PWD isolation. Drop the parser-logic-duplication constant test.
`skill_serve_review` always spawned `open <url>`, so automated tests and
library callers opened the user's browser (many localhost review tabs) and
never closed them.

- `serveReview` already accepted `openBrowser`; make it authoritative and add
  `OPENCODE_SKILL_CREATOR_OPEN_BROWSER=0|false` as a process-level override.
  The interactive default stays true. Add an injectable `openBrowserImpl`
  launcher so tests can assert invocation without spawning anything.
- `skill_serve_review` gains an explicit `openBrowser` argument (default true)
  and passes it through; tool description documents the opt-out.
- Tests: set `OPENCODE_SKILL_CREATOR_OPEN_BROWSER=0` process-wide in the node
  suite, pass `openBrowser: false` in every automated review call, and add
  `test/browser-open.test.ts` proving no launcher invocation when disabled (and
  one when enabled) with no real browser. All suites tear down servers/temp
  dirs in `finally`.
- Record the durable preference in the task worktree AGENTS.md: automated tests
  never open the browser; interactive QA reuses one Ego TaskSpace and closes
  only task-created pages; workers report cleanup.
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

🏹 Robin

✅ Review done. I flagged 7 things worth a look.

Push fixes whenever you like, then comment /robin for another pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏹 Robin

Heads up: this is a point-in-time review. Push fixes freely, then comment /robin whenever you want another pass.

⚠️ 2 Medium | 🔵 3 Low | 💡 2 Suggestions

Summary

This change adds a CI workflow, migrates the installer and docs from the V1 plugin key to the V2 plugins key, replaces the destructive lsof | SIGTERM port cleanup with a loud EADDRINUSE error, adds a browser-open opt-out (option + env var), and splits the plugin into V1/V2 entrypoints with per-instance state and a shared tool registry. The refactor is clean, well-commented, and backed by new tests. The main concerns are an unhandled async spawn error in the browser launcher, a fresh-install gap for V1 users in the installer, and a few smaller consistency/test-hygiene issues.


Robin — the Robin Hood of code review. Free for every PR.

Comment thread plugin/lib/review-server.ts
Comment thread plugin/bin/opencode-skill-creator.js
Comment thread plugin/package.json
Comment thread plugin/test/browser-open.test.ts Outdated
Comment thread plugin/skill-creator.ts
Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml
…l, harden test teardown

Robin round 1 accepted fixes (PR #44).

- review-server.ts: attach an `error` handler to the spawned launcher. A missing
  binary (headless Linux, `open` absent) emits an asynchronous 'error' event
  that the surrounding try/catch cannot catch; without a listener it crashed
  the process. The launcher now forwards async failures via an `onError`
  callback and the review server logs an actionable warning with the manual URL
  instead of dying. Interactive default behavior is unchanged.
- test/browser-open.test.ts: capture `stop` outside the try and always await it
  in `finally`, with env restoration and workspace removal nested so they run
  even if `stop` throws. Adds a regression that points PATH at an empty dir,
  exercises the real `spawn("open", …)` ENOENT path, and asserts the process
  survives, the server still answers 200, and the manual URL is reported. No
  browser is launched.
- README.md / plugin/README.md: state that the automatic installer targets
  OpenCode V2 only, and add an explicit supported-V1 (>= 1.18.29) manual
  singular `plugin` setup with restart. Removes the claim that the same
  automatic install works on the V1 floor; the installer is not changed to
  write both keys.

Verification: build OK (113 KB, no inlined node_modules); npm test 46/46 (real
V1 1.18.29 floor + real V2 runtime); test:ts 82/82; listeners 40->40; global
config hashes unchanged; no browser opened.
@antongulin

Copy link
Copy Markdown
Owner Author

/robin

Round 1 triage complete. Fixed in 97a3e15 (on top of the reviewed 6db0a3f):

  • F1 (MEDIUM) — review-server.ts: the spawned launcher now attaches an error handler and forwards async failures via an onError callback; serveReview logs an actionable warning with the manual URL instead of crashing on an unhandled 'error' event. Regression proves the process survives and the server still answers 200 when the launcher is missing.
  • F2 (MEDIUM) — documentation: the automatic installer is labeled V2 only; plugin/README.md adds an explicit Supported V1 setup (manual) section (singular plugin key + restart) and the "same automatic install works on V1" claim is removed. The installer is not changed to write both keys.
  • F4 (LOW) — browser-open.test.ts: stop is captured outside the try and always awaited in finally, with env restoration and workspace removal still running if stop throws.

Rejected with factual evidence (unchanged): F3 (lock pins 1.18.33 ≥ floor; dev-range churn), F5 (no tool reads the context; V1/V2 contexts are different APIs), F6 (build manifest intentionally includes builtAt; existing sourceHash check), F7 (no tsconfig/TS dependency; adding tooling is out of scope).

Verification on 97a3e15:

  • npm run build OK — dist 113 KB, 0 inlined node_modules.
  • npm test — 46 pass / 0 fail (real V1 1.18.29 floor + real V2 runtime).
  • npm run test:ts — 82 pass / 0 fail.
  • Listeners 40→40; global config unchanged; no browser opened.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

🏹 Robin

✅ Review done. I flagged 9 things worth a look.

Push fixes whenever you like, then comment /robin for another pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏹 Robin

Heads up: this is a point-in-time review. Push fixes freely, then comment /robin whenever you want another pass.

⚠️ 3 Medium | 🔵 4 Low | 💡 2 Suggestions

Summary

This is a well-structured change: it splits the plugin into V1 (server()) and V2 (setup()) entrypoints from a single bundle, moves the installer to the V2 plugins key while preserving legacy plugin entries, replaces the dangerous lsof-based port killing with a loud EADDRINUSE error, adds a browser-open opt-out with a test seam, and makes the skill-conflict guard fail loudly instead of silently skipping. Comments and the new tests are generally good. The main gaps are that CI never verifies the committed dist/ matches a fresh build, the conflict guard now aborts evals whenever skill enumeration is unavailable, and the release process may ship these breaking changes as a patch bump.


Robin — the Robin Hood of code review. Free for every PR.

Comment thread .github/workflows/ci.yml
Comment thread plugin/lib/run-eval.ts
Comment thread .github/workflows/publish.yml
Comment thread plugin/skill-creator.ts
Comment thread plugin/package.json
Comment thread plugin/skill-creator.ts
Comment thread plugin/README.md Outdated
Comment thread plugin/lib/run-eval.ts
Comment thread .github/workflows/ci.yml
… clarify V1 install docs

Robin round 2 accepted fixes (PR #44).

- Version 0.3.0 (was 0.2.25): this line drops the function default export that
  OpenCode V1 < 1.18.29 required and the automatic installer now writes the V2
  `plugins` key, so it is a compatibility-breaking change rather than a patch.
  Set in package.json; the lockfile is updated coherently (version-only, no
  dependency churn) and dist metadata is regenerated through `npm run build`.
  This is explicit version selection for the normal gated merge/automatic
  publisher — no manual npm publish, tag, or GitHub release.
- README.md / plugin/README.md: document that older V1 (< 1.18.29) releases must
  pin the older 0.2.x line, and separate the V1 manual singular-`plugin` setup
  from the V2 installer's preservation of a pre-existing legacy `plugin` array.
  The earlier "do not add both keys" line now explains the upgrade exception and
  warns users to keep only the key their runtime uses.

Verification: build OK (113 KB, 0 inlined deps); version coherent across
package.json / package-lock.json / dist/package.json; npm pack dry-run ships
0.3.0; npm test 46/46 (real V1 1.18.29 floor + real V2 runtime); test:ts 82/82;
listeners 40->40; global config unchanged; no browser opened.
@antongulin

Copy link
Copy Markdown
Owner Author

/robin

Round 2 triage complete. Fixed in 79791b1 (on top of 97a3e15):

  • R3 (MEDIUM) — version raised to 0.3.0 to reflect the compatibility-breaking V1 floor change (function default export dropped; installer targets V2). Lockfile updated coherently (version-only, no dependency churn), dist metadata regenerated via npm run build, and both READMEs document pinning the older 0.2.x line for V1 < 1.18.29. Explicit version selection only — no manual publish/tag/release.
  • R7 (LOW) — docs clarify the V1 manual singular-plugin setup vs the V2 installer preserving a pre-existing legacy array, including the upgrade exception and the warning to keep only the key the runtime uses.

Rejected with factual evidence (unchanged): R1 (sourceHash gate runs before rebuild; builtAt makes the raw diff invalid), R2 (fail-loud enumeration is the intended accepted behavior), R4 (ctx.location.directory is the current API; no unrelated-cwd fallback), R5 (lock pins 1.18.33 ≥ floor), R6 (executors read args only), R8 (parser helper is test-only; runtime guard fails loud), R9 (optional CI cache perf).

Verification on 79791b1:

  • npm run build OK — 113 KB, 0 inlined node_modules; version coherent across package.json / package-lock.json / dist/package.json.
  • npm pack --dry-run ships 0.3.0 (13 files, incl dist/package.json).
  • npm test — 46 pass / 0 fail (real V1 1.18.29 floor + real V2 runtime).
  • npm run test:ts — 82 pass / 0 fail.
  • Listeners 40→40; global config unchanged; no browser opened.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

🏹 Robin

✅ Review done. I flagged 8 things worth a look.

Push fixes whenever you like, then comment /robin for another pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏹 Robin

Heads up: this is a point-in-time review. Push fixes freely, then comment /robin whenever you want another pass.

⚠️ 1 Medium | 🔵 5 Low | 💡 2 Suggestions

Summary

This change adds a CI workflow, migrates the installer to OpenCode V2's plugins key while preserving any legacy plugin array, replaces the dangerous lsof -ti | SIGTERM port-killing with a fail-loud bind, adds a browser-open opt-out with proper async error handling, and refactors the plugin into per-instance state with dual V1/V2 entrypoints. The refactor is well-structured, the docs are updated consistently with the behavior change, and the new browser-open tests cover the important paths. The main gaps are a CI step that does not actually verify the committed dist/ matches a fresh build, plus a few smaller consistency and test-isolation issues.


Robin — the Robin Hood of code review. Free for every PR.

Comment thread .github/workflows/ci.yml
Comment thread plugin/package.json
Comment thread plugin/skill-creator.ts
Comment thread plugin/lib/run-eval.ts
Comment thread plugin/test/browser-open.test.ts
Comment thread plugin/lib/review-server.ts
Comment thread plugin/skill-creator.ts
Comment thread .github/workflows/ci.yml
@antongulin
antongulin merged commit 2e65107 into main Oct 1, 2026
2 checks passed
@antongulin
antongulin deleted the fix/opencode-v2-delivery branch October 1, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant