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.tsmirrors@opencode/aiMessage/ContentPartclosely enough to read and rewrite tool calls, andindex.tsimports the@opencode/plugintypes.npm run check:compatcompiles the plugin against the real SDK declaration (an isolated install, no runtime) and fails when a shape changes, e.g. areadonlyfield.opencode.d.tsis a local shim so the plugin does not depend on the SDK at runtime. - Runtime shape:
tests/fixtures/hook-messages.jsonis 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, runcheck:compatfirst, 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.setat most once in flight) and ordered, a failed write stays retryable, andclose()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.tsthe only place that knows OpenCode message shapes; the vendored library undervendor/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.