Contributing

September 19, 2026 · View on GitHub

Thanks for helping improve fast-jev-opencode. This plugin rewrites the request OpenCode is about to send, so the bar is "never surprise the user": every change must keep the fail-open guarantee below.

Setup

git clone git@github.com:nrdz-labs/fast-jev-opencode.git \
  ~/.config/opencode/plugins/fast-jev
cd ~/.config/opencode/plugins/fast-jev
npm install

There are no runtime dependencies — the plugin uses node: builtins and its own files. npm install is only for typescript and @types/node.

Checks

npm run typecheck   # tsc --noEmit, strict
npm run selftest    # 18 assertion blocks + sanitize/privacy/compat-CLI suites + shape, no network
npm run check:compat -- --version 2.0.8   # real SDK types, run in a temp install
bun livecheck.ts    # one real Jev round-trip (needs TYPESAFE_API_KEY)

CI runs typecheck and selftest; it also runs check:compat against @opencode/plugin 2.0.7 and 2.0.8. livecheck is manual because it needs a key. selftest is offline by construction: it stubs the transport, runs the prune pipeline against an in-memory cache, and uses the committed fixture.

check:compat validates its arguments before installing, installs the pinned SDK into a temp dir that is removed even when the install or compile fails, and reports the version read from the selected SDK's package.json (so --sdk-dir never reports the default). npm run test:compat-cli covers those CLI contracts offline with a stubbed npm.

Type contract vs runtime capture

Two different things can drift, and they are checked differently:

  • Types: types.ts mirrors @opencode/ai Message/ContentPart closely enough to read and rewrite tool calls, and index.ts imports the @opencode/plugin types. npm run check:compat compiles the plugin against the real SDK declaration (an isolated install, no runtime) and fails when a shape changes, e.g. a readonly field. opencode.d.ts is a local shim so the plugin does not depend on the SDK at runtime.
  • Runtime shape: tests/fixtures/hook-messages.json is a capture of one real hook payload (SDK 2.0.7) with a shape signature. It catches drift in the fields the plugin actually reads, but a static fixture cannot detect an SDK upgrade on its own — only the compat check compiles against new SDK types. After an OpenCode upgrade, run check:compat first, then recapture.

The fail-open rule

Any failure — missing key, timeout, transport error, HTTP error, malformed answer, invalid answer, malformed tool payload, unreadable config, unknown message shape — must leave the request untouched and log safely. Never let a new code path throw out of the context hook.

Privacy rule

log.jsonl and console must never receive prompts, tool inputs/results, file contents, the goal, the full baseUrl, API keys or raw error text. Log only fixed categories and allowlisted operational fields: wrap a failure in SafeDiagnosticError (see diagnostics.ts) or log a literal code. Do not String(error) a caught value — a body, URL or cause can ride along. When you add a failure path, add a selftest that puts a sentinel in the input and asserts it is absent from captured output, and extend npm run test:privacy, which runs the real logLine against a temp log.jsonl.

tests/fixtures/hook-messages.json pins the real hook payload; if an OpenCode upgrade changes shapes, the selftest should fail before anything ships.

Fixtures

The fixture is a sanitized capture of a real hook payload. Do not hand-edit it. To refresh it after an OpenCode upgrade, follow tests/fixtures/README.md: enable debugDumpMessages, capture one request, run node scripts/sanitize-fixture.mjs <dump> tests/fixtures, then npm run shape:write. The sanitizer is a schema allowlist: unknown part / result types and extra fields are refused (nothing written) rather than copied, so a new shape means extending the allowlist first. It also refuses to write if it finds paths, real session ids or key material; add capture-specific needles with FIXTURE_FORBIDDEN=needle1,needle2. Refusals print fixed categories and schema-only paths: a host key/type/text and a needle value never appear in stdout or stderr, and read/parse/write failures report a category instead of a V8 excerpt. Review the generated shape.txt diff before accepting it — the signature moving is expected after an upgrade, but the change should match what you intended; then run npm run shape:write. npm run test:sanitize covers the allowlist and the diagnostics.

Runtime guarantees

  • The decision cache is per plugin instance; nothing is shared between projects or reloads. Writes are serialized (storage.set at most once in flight) and ordered, a failed write stays retryable, and close() is idempotent and awaits the last snapshot. The SDK cleanup returns that promise.
  • A prune pass builds the whole rewrite in a new array and publishes it to the hook event only after success. A read-only array or field skips the pass; the original array and its call/result pairs are never mutated, on success or on error.

Style

  • TypeScript strict (noUncheckedIndexedAccess), 2-space indent, double quotes, semicolons, ~100 columns. Match the surrounding code; there is no formatter configured yet, so keep diffs quiet.
  • Comments explain why, one line where possible. Public helpers carry a short doc comment.
  • Keep adapter.ts the only place that knows OpenCode message shapes; the vendored library under vendor/ stays upstream-shaped.

Pull requests

  • One topic per PR, with the selftest (and a fixture/shape refresh, if shapes moved) in the same commit.
  • Say in the description what the change does to the outgoing request under normal operation, and what it does when Jev fails.
  • Update the config reference (docs/CONFIGURATION.md) when you add or change a config field.

Security

Do not open public issues for credential leaks — see SECURITY.md.