jev-bus v1

September 20, 2026 · View on GitHub

This file is identical in jev-context-fabric and jev-prune-kit. It specifies how two independently installed packages transform the same message list without conflicting.

The problem it solves

Both packages want to edit a host's outgoing request. Before jev-bus, both registered OpenCode's experimental.chat.messages.transform and both tried to claim /prune through mechanisms that could not see each other — one writing commands/prune.md, the other setting cfg.command.prune. Two independent mutations of one array, in undefined order.

jev-bus replaces that with: one carrier per host, many ordered stages.

Roles

RoleMeaning
CarrierThe single package that registers the host's transform hook and owns /prune there.
StageA package that contributes an edit inside the carrier's chain.

Every package is a stage everywhere it is installed. A package is additionally the carrier wherever it holds the slot.

Hosts that can carry a chain

pi, opencode, hermes — and no others.

This is a host-API ceiling, not a package limitation. OpenClaw, Codex, Claude Code, Gemini CLI, Cursor and GitHub Copilot CLI expose no outgoing-request transform. Both packages still install MCP, skills and capture there; neither can project. register() refuses a carrier claim for any host outside this set rather than letting an installer imply otherwise.

Carrier ranks

Rank, not install order, decides. Both vendored copies carry the same table, so the two installers reach the same answer whichever runs first, and a tie keeps the incumbent so a reinstall never flips a settled assignment.

PackageHostRankWhy
jev-prune-kitpi100Sole Pi adapter; the other package has no Pi support at all
jev-prune-kithermes100Real llm_request middleware; the other has only shell hooks, which inject evidence and cannot remove a message
jev-prune-kitopencode10A v1-era message transform only
jev-context-fabricopencode50Implements both the V1 and V2 plugin APIs

Outcome: opencodejev-context-fabric; pi and hermesjev-prune-kit.

--force-carrier overrides rank and reports the package it displaced. --no-bus opts out entirely and restores standalone 0.1.0 behaviour, conflicts included.

Claims

A stage declares which message classes it may modify, from a closed vocabulary:

tool-result:read · tool-result:* · assistant-prose · system-append · message-remove · message-reorder

Two different packages claiming overlapping classes on one host is a hard install failure. That is the structural guarantee: double-registration becomes impossible rather than merely discouraged. A wildcard subsumes its family. An unknown claim is refused, never ignored, so a future class cannot silently overlap an old one. One package may hold several claims; only cross-package overlap is a conflict.

PackageClaimsEffect
jev-prune-kittool-result:readSubstitutes duplicate read bodies in place
jev-context-fabricassistant-prose, message-remove, system-appendRemoves approved prose; appends retrieved evidence

Disjoint, so both run in one chain.

Order is load-bearing

Priority 100 (dedup) runs before 200 (view). This is not cosmetic.

jev-context-fabric keys every message as sha256([index, message]), so identity depends on array position. jev-prune-kit substitutes bodies without changing the array length, while jev-context-fabric removes messages. Dedup must therefore run first: it cannot shift the indices the other package keys on, and it only touches tool results, which that package never excludes. Reverse the order and every downstream key would be invalidated.

Ties break by stage name, so an equal-priority pair never reorders between runs.

Stage protocol

Single-shot JSON over stdio, one object in, one object out — the transport both packages already used.

Request

{ "schema": "jev-bus.stage.v1", "op": "transform" | "plan",
  "host": "opencode", "api": "v2", "session": "...", "workspace": "...", "goal": "...",
  "accepts_system_append": true,
  "messages": [...], "original_messages": [...], "notes": [...] }

Response

{ "ok": true, "messages": [...], "system_append": "...",
  "notes": [{ "stage": "...", "action": "...", "count": 3, "bytes": 41822, "detail": "..." }] }

original_messages is the pristine pre-chain array. A stage that keys or fingerprints messages must use it, not the possibly-mutated messages; otherwise an upstream edit re-keys everything and stales every open plan.

accepts_system_append is false when the carrier has no system array in that hook, or already injects evidence through a separate hook. A stage must then return no system_append rather than smuggling the text into the message list.

Fail-safety

A stage may decline. It may never break the host's turn.

A stage that errors, times out, exits nonzero, or returns an unrecognized shape contributes nothing; the chain continues with that stage's own input. Any failure to resolve the chain at all — a claim conflict, an unreadable or malformed registry, even an environment with no resolvable home directory — degrades to passthrough with a chain-refused note.

The bus is additive, never a prerequisite. Every carrier keeps its standalone path: if the chain contributed no stage of its own package, the carrier performs its original direct transform exactly as it did before jev-bus existed. Installing the bus can subtract capability from nobody.

Registry

~/.jev/bus/v1/registry.json, overridable with JEV_BUS_HOME. Written under an exclusive-create lock, atomically, refusing symlinked paths, bounded at 256 KB.

{ "schema": "jev-bus.v1",
  "hosts": { "opencode": { "carrier": "jev-context-fabric", "rank": 50, "api": "v2" } },
  "stages": [
    { "name": "jev-prune.dedup@opencode:1a2b3c4d", "package": "jev-prune-kit",
      "priority": 100, "claims": ["tool-result:read"], "hosts": ["opencode"],
      "transport": { "kind": "subprocess-json", "argv": ["...", "--bus-stage", "..."] } }
  ] }

The registry is additive metadata, never a file-ownership claim. Each package keeps its own manifest and owns only its own files: install-receipt.json for jev-context-fabric, manifest.json for jev-prune-kit. Uninstalling removes only that package's entries, and a vacated carrier slot is left empty rather than reassigned — the survivor must reinstall to take the hook deliberately.

Approval

The bus approves nothing. op: "plan" is a preview that never mutates the array. The carrier renders a combined, package-attributed preview; approval stays in each package's own CLI, with its own consent requirements:

/prune preview — session abc123
  jev-prune.dedup        3  duplicate read bodies       41.8 KB
  jev-context.view       7  assistant-prose messages    12.1 KB
approve:  jev-prune   assess        (needs JEV_PRUNE_ALLOW_REMOTE)
          jev-context prune-apply PLAN_ID --enable-native

Joining a chain never starts a paid call. jev-prune-kit's stage runs only its local project; remote assessment stays behind /prune plus explicit consent.

Native /compact is untouched by both packages, as before.

Vendored file integrity

The contract is vendored, not depended on, because both packages advertise a zero-dependency offline install. Both copies must stay byte-identical; each repository's test suite fails if its copy drifts from these hashes.

Filesha256
bus.py514e59a2776ae1adbed5bbcf8e669d4fa15972877440a15cf8b00516bbfb87b7
bus.mjs907758980cd5939a341eb9ab04bed183c1b928f19e5faad690508581f9882cf0

Located at src/jev_context/bus.py + adapters/bus.mjs in jev-context-fabric, and jev_prune/bus.py + adapters/bus.mjs in jev-prune-kit. Changing the contract means editing both packages and updating this table in both.

What is still not true

  • Nothing here is live-host tested. Every check in both repositories runs against mock hosts. A passing mock certifies the adapter's shape, not the host's behaviour.
  • The bus adds one subprocess hop per stage. The carrier enforces a total chain deadline, so a slow stage degrades to passthrough rather than stalling a turn.
  • Coordination does not create capability. Dedup still cannot project in a host with no transform API, and no measured token or task-quality result is claimed by either package.