@pablozrrrr/jevguard
v0.2.3
Published
OpenCode plugin that reviews a completed agent turn against a repository semantic rule using Jev.
Maintainers
Readme
JevGuard
Semantic policy engine for coding agents.

JevGuard checks the code an agent just changed against the policies that matter in
your repository. It uses Jev for a narrow semantic judgment per rule, then applies
your local deterministic gate to return PASS, WARN, or FAIL.
agent completes a turn
↓
task + attributed diff + local rules
↓
Jev per rule
↓
local policy gate per rule
↓
PASS · WARN · FAILNot a code generator. Not a prose review bot. Not another static linter.
JevGuard is a semantic linter: it asks whether a specific change violated a specific policy, and leaves planning, code generation, and correction with the coding agent.
[!WARNING] JevGuard is in active development. The current release targets OpenCode
1.18.32and evaluates every rule block declared in.jev/rules.mdplus theSCOPE-CREEPandCOMPLEXITYbuilt-ins. Reviews are background and observe-only: they report results but never alter the agent context or block a task. When remediation is enabled and a review has anyFAIL, the plugin asks a hiddenjevguard-proposersubagent for a proposal in an isolated child session and shows a generic toast; the proposer never edits code.
Why JevGuard
Traditional linters are excellent at syntax, types, and patterns. They cannot reliably answer repository-specific questions such as:
- Did a controller start making domain decisions?
- Did this business-rule change arrive without meaningful tests?
- Did the agent solve a small task by introducing an unnecessary abstraction?
- Did a change cross an architectural boundary that this codebase protects?
JevGuard makes those questions versioned, scoped, and machine-actionable.
## ARCH-001
severity: error
scope: backend/**
scope: !backend/generated/**
evidence: code
### Rule
HTTP controllers must not contain business logic.
### Violation
A controller performs domain decisions, calculations, or state mutations directly.
### Allowed
Validation, HTTP mapping and delegation to services.scope: may repeat. Each line adds an inclusion glob, and a leading ! adds an
exclusion, so a path is in scope when it matches at least one inclusion and no
exclusion. evidence: chooses which files the rule is evaluated against — code
(the default), docs, or any. Because the default is code, prose documentation
(.md, .mdx, .txt) no longer participates in a rule's evidence unless the rule
opts in with evidence: docs or evidence: any; this is intentional. Config and
data formats (.json, .yaml, .toml, .xml, .csv, and others) stay in the
code class, so config rules keep their coverage.
The rule stays in the repository beside the code it governs. Allowed is a real
exception, not a suggestion. JevGuard evaluates it as part of the same rule.
The contract
JevGuard is built around one non-negotiable idea: review the change that belongs to the turn, not whatever happens to be in the worktree.
user task
↓
assistant response
↓
attributed patch
↓
applicable rules
↓
one semantic judgment per ruleIf complete attributed evidence is unavailable, invalid, or contains a blocked
sensitive file, JevGuard marks the affected rule UNAVAILABLE. It never turns
incomplete evidence into a reassuring verdict. Evidence problems are scoped to the
rules they affect: a rule whose applicable files are all safe still runs even when
another rule's evidence is blocked.
Oversized evidence is no longer refused outright. A scoped diff above
maxDiffLength is split, in attributed file order, into one slice per file; an
individually oversized file is split into its ordered unified-diff hunks with the file
preamble repeated in every slice. Nothing is truncated, summarized, or dropped: an
unparseable oversized file or an indivisible oversized hunk makes the whole rule
UNAVAILABLE/OVERSIZED_DIFF.
Some failures happen before any policy rule is evaluated. If the attributed diff
cannot be built, the review is a single synthetic UNAVAILABLE entry with no rule
ID, and neither lane runs. If the turn is attributed but the policy files cannot be
read or validated, the rule lane reports one synthetic UNAVAILABLE entry with no
rule ID — it does not invent one result per declared rule — while the built-in batch
still runs against the turn's attributed patch.
How a review works
For every applicable rule, JevGuard sends Jev one focused yes/no question:
Does this attributed change violate
ARCH-001?
Jev returns a Noul: the probability that the answer is yes. JevGuard owns the policy that translates it into an outcome.
| Rule severity | PASS | WARN | FAIL |
| --- | ---: | ---: | ---: |
| error | < 40% | 40%–69% | ≥ 70% |
| warning | < 60% | ≥ 60% | never |
Those thresholds are per repository rule and locally configurable in
.jev/config.yaml. Slicing does not change them: each slice is gated independently,
the rule outcome is the highest slice outcome (FAIL > WARN > PASS), and the rule's
violationProbability is the maximum across its slices. PASS requires every planned
slice to pass. A rule may produce at most 16 slices, and slicing may add at most 32
repository-rule Jev calls per turn beyond one per applicable rule; a rule that does not
fit is UNAVAILABLE/SLICE_LIMIT_EXCEEDED with no Jev call and later rules still use the
remaining budget. Any failed, blocked, or out-of-range slice makes the whole rule
UNAVAILABLE, and no sibling probability is presented as a verdict. The built-ins do
not slice.
JevGuard also runs two product-owned built-ins on every attributed turn,
SCOPE-CREEP and COMPLEXITY. Both receive the turn's complete, safe attributed
patch with no scope filtering, and both are answered by a single Jev request
that carries two independent questions. Each answer is validated on its own, so a
malformed answer fails only its own check.
SCOPE-CREEP asks whether the change contains material functional, behavioral,
architectural, dependency, configuration, documentation, or refactoring work the task
did not request and that is not reasonably necessary to complete it. It uses fixed
error thresholds (65%/90%), independent of .jev/config.yaml.
COMPLEXITY asks whether the change introduces material complexity disproportionate
to, or not reasonably necessary for, completing the task, such as unnecessary
abstractions, layers or indirections without proportional gain, new dependencies
without a clear need, excessive configuration, premature generalization, or structure
materially larger than the problem requires. It is advisory: it warns at 50% and
can never fail.
The rule lane and the built-in batch are launched together. One shared FIFO
concurrency limit caps the plugin instance at two Jev requests in flight, counting the
built-in batch as a single request. Results join the same aggregate in a fixed order:
rules in source order, then SCOPE-CREEP, then COMPLEXITY.
Reviews run in the background. The idle event returns as soon as the serialized attribution step is scheduled; policy reads, Jev calls, and presentation never sit on the agent's critical path.
Safe auto-propose remediation
Reviews are observe-only by default: a review never injects feedback into the agent context, adds a session message, prompts a session, blocks a turn, or changes your code. A separate, configurable path can turn a review result into a proposal, and it is delivered entirely by the same server plugin.
When .jev/config.yaml enables remediation (it is enabled by default) and a review
contains any FAIL, the plugin, after presenting the review, sends one aggregate
proposal request to a hidden jevguard-proposer subagent:
- The request aggregates every
FAILfinding in the review — localerrorrules and theSCOPE-CREEPbuilt-in — together with the turn's task and its complete, safe, full attributed patch.COMPLEXITYis advisory and never fails, andwarningrules never fail. - The full attributed patch must pass the same safety policy as the review. If any
attributed file is blocked or the patch is oversized, the plugin records no proposal
at all — even when a rule-scoped
FAILexists — because there is no complete safe evidence to send. It never falls back to a repository or global diff. - The request is sent in an isolated child session parented to the source session. The
jevguard-proposersubagent has wildcard-deny permissions and wildcard-disabled tools, so it cannot call a tool, edit a file, or produce a patch. It returns a strategy and one manual apply instruction only. remediation.modelis optional. When set in.jev/config.yaml, it selects the proposer model; when it is absent, the proposer inherits the host's model.- A generic toast reports that the proposal is ready in the child session. It carries no rule, task, diff, finding, or credential.
The plugin creates at most one proposal per evaluated turn, and the child session is
excluded from review for the plugin lifetime, so a proposal can never recurse or
trigger a second one. A host, model, or toast failure is contained and never changes
the review. A contained proposal failure — an invalid model specifier, a child-session
failure, or a prompt failure — shows a generic error toast and writes one structured
error log entry carrying only a typed reason code, never any task, diff, finding,
path, or credential.
The proposer is a strategy step, not an apply step: it never edits code, and nothing is applied automatically. You read the proposal, copy its manual apply instruction into your normal coding agent, and apply it there. That apply is an ordinary completed assistant turn, so the server plugin reviews it normally on the next idle — exactly like any other turn.
The proposal prompt treats the task, paths, diff, rules, and findings as untrusted data, and requires a separate explicit confirmation before proposing changes to tests, configuration, or dependencies. No secret is ever carried, and no payload, task, diff, or secret is logged.
Install
Installing @pablozrrrr/jevguard does not register it with OpenCode by itself.
OpenCode only loads plugins listed in plugin: [...] in opencode.json, so register
the plugin from the installed CLI:
# global config: ~/.config/opencode/opencode.json
jevguard install
# project config: ./opencode.json
jevguard install --projectThe installer asks for confirmation, edits only the plugin array, and is
idempotent. It only edits a plugin array of plain strings, so OpenCode's
[["pkg", { "options": {} }]] tuple form is a known limitation: the installer leaves
the file unchanged and prints the manual snippet instead. If jevguard is not on
PATH, run it through bunx:
bunx --package @pablozrrrr/jevguard jevguard installThe manual alternative is one entry in opencode.json:
{ "plugin": ["@pablozrrrr/jevguard"] }OpenCode resolves the package from the npm registry or its cache and loads it on the
next start. Adding the entry only loads the plugin; it does not put the
jevguard CLI on PATH (see Install the CLI). Restart OpenCode
after changing the plugin list.
[!IMPORTANT] A bare plugin entry resolves only from the npm registry. To run an unreleased local build, load the packed tarball as a local artifact plugin instead.
Build the local artifact
The artifact ships the plugin as a locally packed tarball that bundles the
private @jevguard/core and @jevguard/opencode-adapter workspace code, so a
clean consumer installs one tarball and never resolves a workspace link or a
private registry package. It also packages the jevguard-rules and jev-init
skills and their private validators under skills/<name>/.
Build the tarball from this repository:
corepack enable
pnpm install
pnpm artifact:build
pnpm artifact:packpnpm artifact:pack writes artifacts/pablozrrrr-jevguard-<version>.tgz. Both
packages/plugin/dist/ and artifacts/ are build output and are not committed.
The packed manifest depends only on public runtime packages
(@inquirer/password, @napi-rs/keyring, @typesafe-ai/sdk, yaml). Installing
the tarball therefore needs npm registry access for those packages; the artifact
does not vendor them and does not promise an offline install.
Load the plugin
OpenCode 1.18.32 resolves a bare opencode.json plugin entry from the npm
registry or its cache:
{
"plugin": ["@pablozrrrr/jevguard"]
}To run an unreleased local build instead, install the packed tarball into the
project's .opencode directory and load it through a local plugin shim:
cd /path/to/consumer/.opencode
bun add /path/to/pablozrrrr-jevguard-<version>.tgz// .opencode/plugins/jevguard.ts
export { JevGuardPlugin } from "@pablozrrrr/jevguard";OpenCode loads .opencode/plugins/ with its bundled Bun runtime. The shim runs as
a local plugin module and resolves @pablozrrrr/jevguard from
.opencode/node_modules. Use either the bare plugin entry or the local shim, not
both.
Remediation is built into the same server plugin: the config hook registers the
hidden jevguard-proposer subagent, so no second entrypoint or shim is needed. The
server plugin reviews and reports turns, and — when remediation is enabled and a review
fails — creates the proposal child session automatically.
Author rules with the bundled skill
The package ships two OpenCode skills beside the plugin: jevguard-rules to author
rules, and jev-init to bootstrap a policy where none exists. On startup the plugin's
config hook appends the installed skill directories to the host's skills.paths, so
the skills are discovered without a manual opencode.json entry. Skill discovery is
read at OpenCode startup: restart OpenCode after installing or updating the package so
the skills appear.
jev-init runs only when neither .jev/rules.md nor .jev/config.yaml exists. It
reads a bounded inventory of the repository, keeps rules only from the user's explicit
constraints and strongly evidenced local conventions, records provenance in a preamble,
and previews both candidate files before requiring explicit confirmation naming both
exact paths. Once a policy exists, every rule change belongs to jevguard-rules.
Policy content is different. .jev/rules.md and .jev/config.yaml are read once
per attributed turn, so editing rule text takes effect on the next reviewed turn
without restarting OpenCode. The skill validates a candidate with the same parser
JevGuard uses, requires explicit final confirmation, and replaces .jev/rules.md
only after the candidate passes; it never edits .jev/config.yaml and never calls
Jev.
Install the CLI
Loading the plugin does not expose the jevguard binary on PATH. Run the login
command without a global install:
bunx --package @pablozrrrr/jevguard jevguard loginOr install the package globally so jevguard is on PATH:
bun install --global @pablozrrrr/jevguard
jevguard loginTo run an unreleased local build, install the packed tarball globally instead:
bun install --global /path/to/pablozrrrr-jevguard-<version>.tgz
jevguard loginKnown limitations:
- Target host is OpenCode
1.18.32. Exact1.18.32runtime smoke is still pending; a local1.18.28run worked. - The plugin and CLI run on Bun, and the packed
jevguardbin keeps a Bun shebang. Install and run the artifact with the Bun runtime.
Connect Jev
For local use, connect once with the installed command:
jevguard loginLoading the plugin does not put jevguard on PATH. If it is not installed
globally, run the one-line login instead:
bunx --package @pablozrrrr/jevguard jevguard loginJevGuard stores the key in your operating system's credential store instead of a
repository file or shell history. In CI, provide TYPESAFE_API_KEY through the
platform secret manager. See the security model for details.
JevGuard FAIL
pass 1 · warn 1 · fail 1 · skipped 0 · unavailable 0One toast per turn shows the aggregate outcome and the outcome counts; the structured log carries every rule's result, its raw violation probability or typed reason, and its scoped paths. The counts include every entry, including the single synthetic entry a review-level failure produces.
PASS, WARN, and FAIL are per-rule semantic outcomes. SKIPPED means that
rule had no applicable change to evaluate. UNAVAILABLE means JevGuard could not
safely or completely evaluate that rule.
Review history
JevGuard keeps a bounded, local, content-free history of your reviews so you can see how turns fared without grepping the host log. Every review appends the same safe structured-log projection plus an ISO 8601 timestamp to one JSON Lines file:
<XDG_DATA_HOME or ~/.local/share>/jevguard/reviews.jsonlThe file holds the aggregate counts, each result's rule or check ID, its outcome, and its typed reason or raw probability — the same allowlist as the structured log. It never holds a task, diff, prompt, model output, credential, or environment value. It is local-only, bounded (it rewrites itself once it passes 5 MB, keeping the newest records), and safe to delete at any time; the next review recreates it.
# human-readable summary
jevguard report
# the same aggregation as JSON
jevguard report --jsonThe report prints aggregated counts, rule IDs, check IDs, and reason codes only — never file paths, tasks, diffs, or credentials. A missing or empty history is a normal empty report, not an error.
Implemented behavior
The current release implements the full local review path:
- OpenCode V1 plugin loads.
- A completed assistant response is detected.
- The direct parent user task and assistant-attributed diff are acquired.
- Every
## <RULE-ID>block in.jev/rules.mdis parsed in source order. Each block is validated independently, so one invalid block does not suppress its valid siblings, and every occurrence of a duplicated ID is invalid. - Each valid rule is processed independently. Its complete scoped evidence is planned locally, then every planned slice across all rules is dispatched through one batched Jev entry that resolves the credential once and uses bounded concurrency; each slice asks one Noul. The rule result is the deterministic maximum of its slice judgments, and a fitting rule still issues exactly one call. The rule asks Jev only when the gate config is valid, the rule is applicable, and its scoped evidence is complete and safe; it produces one typed violation probability or an operational outcome.
- A failure before rule evaluation — no attributed diff, unreadable policy files, or
a missing
.jev/rules.md— is reported by the rule lane as one syntheticUNAVAILABLEentry with no rule ID, not one result per declared rule. When the turn itself is attributed, the built-in batch still runs. - The product-owned
SCOPE-CREEPbuilt-in runs on every attributed turn over the turn's complete, safe attributed patch, with fixederrorthresholds (0.65/0.90) and no scope filtering. It can producePASS,WARN, orFAIL. - The product-owned
COMPLEXITYbuilt-in runs on every attributed turn over the same complete, safe attributed patch. It uses a fixed advisory threshold of0.50, independent of.jev/config.yaml, and can producePASSorWARNbut neverFAIL. - Both built-ins are sent as one batch request with two independent named answers.
One malformed or missing answer fails only its own check; the valid sibling still
gates. A failed or malformed batch envelope makes both checks
UNAVAILABLE. - The built-in batch runs concurrently with the rule lane. The rule lane dispatches
one batched request for all of the turn's planned slices with a bounded adapter
concurrency cap of four; the built-in batch keeps its own single request. One shared
FIFO concurrency limit allows at most two Jev entries in flight across the plugin
instance, counting the rule batch and the built-in batch as one entry each. With no
attributed patch a built-in is
SKIPPED; blocked or oversized evidence isUNAVAILABLE; a rule, policy-load, or config failure never suppresses the batch, and one built-in's answer never suppresses the other. - Reviews run in the background. The idle event resolves as soon as the serialized attribution step is scheduled, so policy reads, Jev calls, and presentation do not block the agent.
- The local gate maps each rule to
PASS,WARN, orFAIL; a rule with no applicable scope isSKIPPED, and a rule that cannot be safely evaluated isUNAVAILABLE. - One aggregate TUI toast and one structured log entry report the turn. The log
carries every entry's outcome, raw probability, or reason, including the built-in
results and the synthetic review-level entry when there is one. The fixed result
order is rules in source order, then
SCOPE-CREEP, thenCOMPLEXITY. - After presentation, when remediation is enabled and the review contains any
FAIL, at most one aggregate proposal request is sent to the hiddenjevguard-proposersubagent in an isolated child session parented to the source session, and a generic toast reports that the proposal is ready. The request carries everyFAILfinding (localerrorrules and theSCOPE-CREEPbuilt-in), the task, and the complete, safe, full attributed patch; a blocked or oversized patch records no proposal. A notifier or host failure never affects the review, presentation, or later turns.
The review itself injects no feedback into the agent session, prompts no session, and blocks no task. The proposal above is a strategy only; nothing is applied, and the user copies its manual apply instruction into their normal coding agent, whose ordinary turn is reviewed normally.
Roadmap
The multi-rule, scope-creep, and complexity slices are implemented. Planned work beyond them:
V0.3 Remaining built-in semantic check: test adequacy
↓
V0.4 jev-init: evidence-based policy bootstrap
↓
V0.5 On-demand evidence and explanations
↓
V0.6 Opt-in, bounded auto-remediation
↓
V0.7 Observe / enforce / remediate modes
↓
V0.8 Rule packs
↓
V0.9 Local feedback and calibration
↓
V1 Claude Code and Codex adaptersThe auto-propose slice is implemented: after a review with a FAIL, an enabled
configuration asks the hidden jevguard-proposer subagent for a strategy in an
isolated child session, and never applies a change. The V0.6 entry remains the
future, unattended corrective loop; the proposer is a bounded, strategy-only step, not
that loop.
The V0.4 jev-init skill is implemented: when no policy exists it bootstraps
.jev/rules.md and .jev/config.yaml from the user's explicit constraints and
strongly evidenced local conventions, records each rule's provenance, and writes both
files only after validating the confirmed candidates.
CI and pull-request policy review come after V1, using the same versioned rules.
Architecture
JevGuard is a pnpm TypeScript monorepo. The core has no dependency on OpenCode or its runtime, which keeps the evaluation model portable to future adapters.
packages/
├── core/ domain, policy, evaluation, gate
├── opencode-adapter/ OpenCode attribution and presentation
├── plugin/ OpenCode composition root and CLI
└── testkit/ fixtures and contract helpers
plugin → opencode-adapter → coreStatus
The multi-rule, multi-built-in, background and observe-only review is implemented. It
loads in OpenCode, attributes one completed turn, parses every rule block in
.jev/rules.md, asks Jev once per applicable rule and once per built-in batch, runs
the SCOPE-CREEP
and COMPLEXITY built-ins over the complete attributed patch behind one shared
concurrency limit, applies the local gate to each, and presents one aggregate result
as a transient TUI toast and a structured log entry.
The local, private tarball (pnpm artifact:build, pnpm artifact:pack) packages
that slice so a clean consumer can install it without workspace links.
OpenCode sees only transient toasts, structured logs, and — when remediation is
enabled and a review has a FAIL — one proposal child session and a generic toast.
The review does not inject anything into the agent context, prompt the source session,
add session messages, or block a task. The proposal is a separate, configurable,
strategy-only workflow delivered by the server plugin; it never edits code and never
applies a change. Real-host validation against exact OpenCode 1.18.32 is still
pending.
Development
Requirements: Node.js >=22.13.0, pnpm 10.33.2 (via Corepack), and the Bun
runtime for the CLI and OpenCode plugin loading.
corepack enable
pnpm install
pnpm typecheck
pnpm testUse pnpm check to run format, lint, typecheck, and tests in sequence. See
CONTRIBUTING.md for the full contribution workflow.
Documentation
License
MIT — see LICENSE.
