feat(opencode): repo-local Impeccable design-hook plugin#191
Conversation
Bridges OpenCode's tool.execute.after into the shared Impeccable hook.mjs brain so file-mutating edits get in-session design feedback, mirroring the Copilot hook. Advisory-only, ~5s timeout, fail-loud on bridge errors. Pure helpers and an injectable detector runner keep the logic Node-testable; the Bun-$ runner is wired only in the exported plugin factory.
fro-bot
left a comment
There was a problem hiding this comment.
Reviewed the diff directly (no ce:* skills invoked). This PR adds a repo-local OpenCode plugin (.opencode/impeccable/) that bridges to the existing, unmodified hook.mjs design detector. Verified locally: vitest run .opencode/impeccable/plugin.test.ts → 31/31 pass; tsc --noEmit -p .opencode/tsconfig.json and root tsc --noEmit → clean; pnpm install --frozen-lockfile --dry-run → lockfile consistent, supply-chain policy passes.
Verdict: PASS
Blocking issues
None.
Non-blocking concerns
hook-bridge.tswithTimeout(lines 143-153): on timeout the underlyingrunDetectorpromise/subprocess is not cancelled or awaited — it keeps running detached. Bounded by the 5s timeout and low call frequency, so impact is minor, but worth a follow-up if tool-call volume ever gets high (e.g. rapid multi-file edits could pile up orphanednode hook.mjsprocesses).plugin.tscreateDefaultRunner(line 27) spreads the fullprocess.envinto the detector subprocess. Not a live risk here —hook.mjsis a trusted, unmodified in-repo script, this plugin only runs in a developer's local Bun/OpenCode host (never in the Node-24 server, CIimpeccable detectgate, or the shipped app), and the script doesn't echo env back to stdout/stderr. Flagging only because the repo holds a GitHub App private key elsewhere — no action needed unless this pattern is later reused somewhere with a narrower trust boundary.vitest.config.tsbroadenedexcludefrom'node_modules'to'**/node_modules/**'— correct given the new.opencode/impeccable/**/*.test.tsinclude, but double-check it doesn't unintentionally widen matching elsewhere (looked fine on the dry run above).
Missing tests
None — plugin.test.ts covers the pure bridge logic thoroughly: tool-name matching (bare + MCP-suffixed), path extraction for edit/write/apply_patch, payload shaping, timeout degradation (no-op, no throw), fail-loud on nonzero exit and runner throw, single-warning-per-session dedup, and stdout parsing (JSON envelope, snake_case, plain text, empty/whitespace). The Bun-only plugin.ts shell (createDefaultRunner) is intentionally left untested under Node, which is reasonable since it's a thin, side-effect-only wrapper verified manually per the PR description.
Risk assessment: LOW
Fully isolated dev-only tooling: no changes to src/ (server) or web/ (client), correctly excluded from the server's strip-only tsconfig.json/ESLint (verified: eslint reports "ignored" for both new files, both tsc invocations pass clean), advisory-only (never blocks a tool call, fails loud instead of silently swallowing errors), and the only production-facing surface is the lockfile/config diff. @opencode-ai/plugin is added as type-only (confirmed import type usage) so it carries no runtime footprint. No auth, secrets, or API-contract surfaces touched.
Run Summary
| Field | Value |
|---|---|
| Event | pull_request |
| Repository | fro-bot/dashboard |
| Run ID | 29129987797 |
| Cache | hit |
| Session | ses_0b1acb22fffeX6ud3ZqZas0da1 |
Adds a repo-local OpenCode plugin that runs Impeccable's design detector after file-mutating tool calls and surfaces any findings back to the agent in the same turn — giving OpenCode the in-session design feedback the Claude/Codex/Cursor/Copilot harnesses already get. Advisory only; it never blocks an edit.
How it works
The plugin bridges OpenCode's
tool.execute.afterhook into the same shared brain the Copilot hook uses (.agents/skills/impeccable/scripts/hook.mjs) rather than reimplementing detection. On a file-mutating tool it shapes the touched path into the eventhook.mjsexpects, runs it, and appends the human-readable findings to the tool result. Clean edits stay silent; a broken bridge fails loud instead of masquerading as "no findings"; detection is bounded by a ~5s timeout. Because it depends only onhook.mjs's stable contract and lives outside Impeccable's four managed harness targets, it keeps working acrossnpx impeccable update.Third runtime, isolated from the app
The plugin runs in OpenCode's Bun plugin host — a distinct runtime from the Node-24 strip-only server (
src/) and the Vite/React client (web/). It never executes in the server, the web bundle, the Docker image, or the CIimpeccable detectgate. To keep it cleanly separated:.opencode/impeccable/is excluded from the server's strip-onlytsconfig.jsonand ESLint config and given its own.opencode/tsconfig.json(full TS is valid there).hook-bridge.ts(Node-testable); the Bun-$runner and single-function plugin export live inplugin.ts.@opencode-ai/pluginis added as a type-only devDependency.Verification
Verified live in a real OpenCode session: a design issue surfaces clean feedback text in the tool result, a clean edit produces nothing and is never blocked, and the plugin files are byte-identical after
npx impeccable update. Gates green:check-types(server + web +.opencode), lint, and the full test suite (31 plugin unit tests covering the bridge logic and state machine).The only CI-facing surface is the config/lockfile changes; the plugin itself is dev tooling.