Skip to content
Back to skills

Jev

ASecurity

Activate, configure, check, or read the tracking loop of the opt-in Jev classifier (TypeSafe's "System One" decision model, reached via the Vercel AI Gateway or TypeSafe's direct API). Use when the user says "activate jev", "activate jev please", "enable jev", "turn on jev", "set up jev", "jev status", "disable jev", "jev report", "set jev key", "how is jev doing", "jev scorecard", "show me the jev report", "label that decision right/wrong", "was jev right", "promote <integration>", "turn <in...

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 2, 2026
ai-agentsrustgoshellbashnodedebugginggitapibackendperformance

Works with

  • claude code
  • cli
  • api

Security analysis

A100/100

Scanned October 3, 2026

npx -y skills add talas9/anti-hall --skill jev --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Jev?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Jev
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/talas9-jev/badge)](https://www.skillsdirectory.com/skills/talas9-jev)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: jev
description: Activate, configure, check, or read the tracking loop of the opt-in Jev classifier (TypeSafe's "System One" decision model, reached via the Vercel AI Gateway or TypeSafe's direct API). Use when the user says "activate jev", "activate jev please", "enable jev", "turn on jev", "set up jev", "jev status", "disable jev", "jev report", "set jev key", "how is jev doing", "jev scorecard", "show me the jev report", "label that decision right/wrong", "was jev right", "promote <integration>", "turn <integration> on/off", "jev budget", "jev credit balance", "jev cost", or "what does <integration> do".
---

# Jev

Jev is anti-hall's opt-in classifier backend (see `docs/KB-jev-classifier.md` in the anti-hall source repository for the
full design). It is **default OFF** and, once enabled, only ever ADDS or RELAXES a
decision within a trust rule the caller controls — see the KB doc's "Fallback
semantics" section. This skill is the activation/config/status front door;
`scripts/jev-setup.js` does the actual work and never prints the key.

## The credential never travels through the model

The key must reach `set-key` over **STDIN only** — never as a chat message the
model repeats, never as a CLI argument (visible in `ps`/shell history), never
logged. Whichever path below is used, once `set-key` reports
`key saved for <transport>`, **never repeat the key back, log it, or write it anywhere
else.**

## Primary flow: "activate jev" / "enable jev" / "set up jev"

1. Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" status` first.
2. **If a key is already present** (`key present: yes`, or the user says they
   already stored one through the plugin options): skip straight to step 5
   (enable) — don't ask for a key again.
3. **If no key is present:** ask which provider the key is for — there is no way
   to guess this from the key's shape, so always ask, never infer:
   - **Vercel AI Gateway** (default, recommended) — the only transport this build
     has live-verified end to end.
   - **TypeSafe direct** — supported by `jev-client.js`'s `typesafe` transport, but
     **not** independently live-verified in this build (TypeSafe's own console has
     closed sign-ups as of this writing — see `docs/KB-jev-classifier.md` §8). Say
     this plainly if they pick it.
4. **Preferred: the plugin option.** Ask the user to store the key themselves in
   the plugin's options screen (Claude Code prompts for these options when the plugin is enabled; they can be changed later from the plugin's configuration) (anti-hall -> `jev_vercel_api_key` for Vercel, or
   `jev_typesafe_api_key` for TypeSafe; the value is kept in the OS credential
   store and sent only to that vendor). The key never appears in this chat and
   you never read it. Note that `status` and `test` run from a shell and cannot
   see a key stored this way (they print "no Jev key visible to this process");
   the hooks do see it.

   A key stored through the plugin options is kept in the OS credential store and is
   visible only to hook processes. A shell command you run (including `jev-setup.js
   status`) cannot see it and will report no key, so do not treat that as proof it is
   missing. Confirm it afterwards from the hooks' own evidence: after the first
   classified hook call, read the per-transport rows of the `jev-report` output
   (calls / errors, where a failed call's reason can be `no-key`).

   **Legacy alternative (opt-in):** the key file written by `set-key` is read
   by the hooks only when `jev.allowLegacyKeyRead` is on (home settings only,
   `--confirmed`). If the user chooses that route, ask for the key in plain
   prose (a secret is not a multiple-choice question) and have them run it
   themselves with the `!` prefix, which reads it with echo off and pipes it
   straight to `set-key`:

   ```
   ! read -rs K && printf '%s' "$K" | node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" set-key --transport vercel && unset K
   ```

   (swap `--transport vercel` for `--transport typesafe` if that's what they
   picked). If they paste the key into chat instead, pipe it to `set-key` via
   stdin exactly as typed, and **never echo it back or repeat it in your own
   reply**:

   ```
   printf '%s' "<the key they pasted>" | node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" set-key --transport <vercel|typesafe>
   ```

   Confirm only with the script's own output — never with the key itself.
5. Enable: `node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" enable [--transport vercel|typesafe]`
   (pass `--transport` again if the user picked typesafe, so it's recorded even on
   a repeat run).
6. Test: `node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" test` — this makes
   **one** real classification call with a fixed harmless question and prints
   `ok`/latency/confidence, or a failure reason. It never prints the key. With a
   key stored through the plugin options it cannot run (a shell sees no plugin
   option); skip it and confirm via `jev-report` after the first hook call.
   - **If it fails with `http-401`/`http-403`** (key rejected): say so plainly,
     ask them to double check they picked the right provider (a Vercel key won't
     work against TypeSafe's direct API and vice versa) and whether the key itself
     is correct, then offer to `set-key` again with a fresh key.
   - Any other failure (`timeout`, `network-error`, ...): report the reason
     verbatim; it's informative and never sensitive.
7. Show `status` again so the user sees the final state (enabled, transport, key
   present, integration modes, last-24h call count).
   - Tip: if the owner knows their per-call rate, set `costPerCall` in
     `~/.anti-hall/jev.json` now so `jev report` can estimate spend — see Costs below.

## "jev status"

Just run `status` and report enabled/transport/key-present/integration
modes/24h call count. Also shows the Vercel AI Gateway credit balance (`GET
/v1/credits`, verified at
https://vercel.com/docs/ai-gateway/sdks-and-apis/rest-api#check-credit-balance)
when the transport is `vercel` and a key is present — served from a 15-minute
cache, so `status` itself only sometimes makes one real request. TypeSafe's
own direct API documents no equivalent endpoint (checked
https://docs.typesafe.ai/api), so nothing is shown for the `typesafe`
transport — never invented.

## Backup transport ("jev fallback")

`jev.fallbackTransport` (`none` default | `vercel` | `typesafe`) makes the OTHER vendor an automatic
backup: when the primary times out, has a network error, returns 5xx (incl. 529), 402 or 429, or a
400/403 whose body names insufficient balance/credits, ONE retry goes to the backup inside the SAME time
budget (a retry with under ~150 ms left is skipped; the primary is held back ~600 ms of the budget for it).
401/403 and other 4xx never fall back, on purpose: a rejected primary key must surface, not be masked.
After 3 consecutive eligible failures a per-vendor circuit breaker skips that vendor for 5 minutes, then probes it
again (state: `~/.anti-hall/cache/jev-breaker.json`).

- Set: `node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" enable --transport typesafe --fallback vercel`
  (`--fallback none` turns it off; a fallback equal to the primary is treated as none).
- Keys are VENDOR-BOUND: a key is never sent to a vendor it was not entered for. Store one per vendor: the
  `jev_vercel_api_key` and `jev_typesafe_api_key` plugin options (or, with `jev.allowLegacyKeyRead`,
  `jev-setup.js set-key --transport <vendor>` writing that vendor's key file, `~/.config/vercel/ai-gateway-key` or
  `~/.config/typesafe/key`; `--role fallback` is an alias for the fallback vendor). The older generic `jev_api_key`
  (and an explicit `jev.keyFile`) still work but are bound to ONE vendor: the home-only setting
  `jev.genericKeyVendor` (default vercel; never `jev.transport`, never env or `/config`); for any other vendor they
  are refused with a "jev_api_key is bound to X; set jev_<Y>_api_key" diagnostic. `enable --transport V` warns when
  V has no key of its own and the generic key is bound elsewhere, and never re-binds. Re-bind deliberately with
  `jev-setup.js bind-generic-key --vendor <v>`. So neither a transport change nor an env/plugin-option flip can
  redirect a key.
- `status` shows both transports and whether a key is visible for each (yes/no only); `test` tests each
  transport on its own.
- NOT full redundancy: the two routes very likely reach the SAME TypeSafe model (inference from the model ids
  `typesafe-ai/jev` vs `jev-1.13.0` and identical answers on a 40-item test; not confirmed). The backup covers the
  direct account's balance/quota and a direct-endpoint outage, probably NOT an outage of the model itself. When both
  fail, the breaker opens for BOTH vendors (calls skip Jev for the cooldown, no double timeouts) and the guards use
  their built-in rules, exactly as when Jev is off.
- Privacy: with a backup on, the same (secret-scrubbed) decision text can reach the second vendor.
- Decision rows in `jev-assist.ndjson` carry `transport` (and `fellBack: true` when the backup served it).
- `jev-report.js` prints a "by transport" block (calls, errors, average latency, fell-back count per vendor; rows logged before transport tracking show as "unrecorded", vercel assumed). `jev.prices` entries are keyed by the response model, which differs per vendor (`jev-1.13.0` vs `typesafe-ai/jev`): list both or use `default`. Only Vercel has a balance endpoint (`status` says "not available" for typesafe).
- UNVERIFIED: which status each vendor returns for an exhausted balance (Vercel `402` per community
  reports; TypeSafe undocumented), so both 402 and 429 are treated as eligible.

## "disable jev"

`node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" disable`. Mention the
per-session alternative: `ANTIHALL_JEV=0` force-disables for one session without
touching `jev.json`.

Text sent to the gateway (prompt, last assistant message, commit/PR text, test output) is passed through a best-effort redactor first: text matching known token shapes (API keys, Bearer tokens, `password=` style assignments, PEM blocks, JWTs, URL credentials, emails, long token-like runs) is replaced with `[REDACTED...]` placeholders. Redaction is best-effort, not a guarantee.

## Costs

Jev is billed per call by the gateway/provider (Vercel AI Gateway passthrough
pricing, or TypeSafe's own pricing for the direct transport). Two cost signals:

- **Estimated** (manual): if the owner knows their per-call cost, they can set
  `costPerCall` in `~/.anti-hall/jev.json` so `jev report` (below) can estimate
  spend; otherwise it reports cost as `n/a`.
- **Real** (automatic, when available): every Jev call already parses its own
  response for a gateway-reported cost or token usage (see
  `hooks/lib/jev-client.js`'s `extractCostAndUsage` — verified against Vercel AI
  Gateway's docs at
  https://vercel.com/docs/ai-gateway/sdks-and-apis/rest-api#look-up-a-generation).
  The TypeSafe "systemone" endpoint this build calls does **not** currently return
  those fields, so real cost is `n/a` out of the box. If the owner knows a
  per-token rate instead, set `jev.prices` in `~/.anti-hall/settings.json`
  (`{"jev": {"prices": {"typesafe-ai/jev": {"inPerMTok": 0.5, "outPerMTok": 1.5}}}}`,
  or a `"default"` entry; file-only — there is no CLI `set` for it; a `prices` key in
  the legacy `jev.json` is still read) and `jev report` computes real cost from actual token counts when the
  response includes them. Real cost NEVER costs an extra network call and NEVER
  charges a cache hit. `jev report --window 24h|7d` shows real cost totals,
  $/call, and $/changed-decision per integration (default: both windows).

## Budget watch (opt-in, observability only)

Set the `jev.budget.*` settings to watch real spend against a cap (via
`/anti-hall:settings`, e.g. `set jev.budget.mode watch`, `set jev.budget.usdPerDay 5`):
`jev.budget.mode` (`unlimited`/`watch`), `jev.budget.usdPerDay`,
`jev.budget.usdPerWeek`, `jev.budget.minCreditUsd`. A legacy `jev.json`
`{"budget": {"mode": "watch", "usdPerDay": 5, ...}}` still works and is migrated into
`settings.json` once.

`mode` defaults to `"unlimited"` (no watching, no warnings). In `"watch"` mode,
`usdPerDay` is required and `usdPerWeek`/`minCreditUsd` are optional. When the
day's real spend exceeds `usdPerDay`, the assist layer logs ONE warning per
calendar day to `jev-assist.ndjson` (`type:"budget-warning"`) — there is no
existing Jev user-facing notice path in this build, so the warning surfaces
only through `jev report` (below), never injected into a hook's own output.
`minCreditUsd` triggers the SAME once-per-day cadence, but for the Vercel
credit balance (vercel transport only) instead of spend — checked at report
time, never in the hook path (see "jev status" above). **Jev is never
auto-disabled by a budget, in any mode** — a human decides whether to act on
it.

## "how is Jev doing" / "jev scorecard" / "show me the jev report"

This is the full tracking loop, end to end — run it whenever the user asks how
Jev is doing:

1. Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js"` (add `--window 24h|7d`
   to narrow the cost window; default shows both).
2. For EACH integration row, read its **suggestion** column and explain what it
   means and what to do next:
   - **KEEP** — changed-decision rate ≥5% AND good-outcome rate ≥80%, with a real
     outcome signal AND a labelled sample (tp+fp, human+auto) of at least 20
     (`MIN_LABELED_FOR_VERDICT`). Tell the user it's earning its keep; no action
     needed (already `on`, or a good candidate to promote from `shadow` to `on`
     — see "Per-integration modes" below).
   - **REMOVE** — ≥200 calls, a labelled sample of at least 20, AND either <60%
     good-outcome rate or >20% failure rate. Tell the user Jev is not adding
     value here; offer to set its mode to `off`. A <1% changed-decision rate is
     LOW YIELD ONLY (v0.108.1 fix) — it shows up as a note on the verdict, never
     as a REMOVE reason by itself.
   - **REVIEW (not enough data: N < 50 calls)** — too early to judge; explain
     more calls are needed (or the owner can `label` some decisions manually to
     seed outcome signal faster — see "labeling" below).
   - **REVIEW (needs labels: n/20)** — v0.108.1 fix: KEEP and REMOVE are BOTH
     withheld below a labelled sample of 20, regardless of how the raw
     changed/good-outcome/failure rates look. Tell the user to run `jev report
     label <hash> tp|fp` on a few more decisions (see "labeling" below) to seed
     the sample, or wait for more `recordOutcome` signal to accrue.
   - **REVIEW (label-only, no outcome signal yet)** — a `choice`/string-label
     classifier (no boolean baseline, e.g. `newRequest`, `supervisorBlockerLabel`,
     `triage`)
     never uses changed-decision rate at all; explain it needs human labels
     before any KEEP/REMOVE verdict is even possible. Its `changed%` column
     shows `n/a (N distinct)` instead of `0.0%` (v0.108.3 fix — a bare 0 read
     as "Jev never did anything" when there's simply no boolean outcome to
     diff), and a `label-only integrations` note below the table (and in the
     headline) spells out the real distinct-decision count split fresh vs
     cache, e.g. `label-only: no boolean outcome to compare; 25 distinct
     decisions (25 fresh)`. This verdict only holds while `labeledSample` is
     still 0. Once the owner runs `jev report label <hash> tp|fp` on a
     would-change choice decision (the decision's own `wouldChange`/`changed`
     was truthy on a fresh call — not just any labelled hash), that tp/fp
     joins the SAME precision/labelled-sample pipeline a boolean integration
     uses (v0.108.3 fix — these used to be read and silently dropped, so a
     `choice` integration could never reach KEEP/REMOVE no matter how many
     labels it got). `changed%`/`changedUnique` still stay 0 (a choice answer
     has no added/relaxed/changed semantics), but `labelWouldChangeUnique`
     (JSON output) tracks the would-change label-candidate count, and once
     `labeledSample` reaches 20 the row falls through to the normal
     REVIEW-needs-labels/REMOVE/KEEP checks — KEEP for a choice integration is
     gated on its own would-change rate in place of changed-decision rate.
   - **REVIEW (p95 latency exceeds budget)** — the classifier is slow relative
     to its own hook's timeout budget; flag it, but this is a performance note,
     not a correctness one.
   - Any other REVIEW — summarize why (usually: some data exists but doesn't
     yet clearly meet KEEP or REMOVE).
3. Mention the **headline** one-liner for each integration (see below) if the
   user wants a quick summary instead of the full table.
4. If real cost or credit balance data is present, surface it (see Costs /
   Budget watch below) — especially a LOW CREDIT warning, which the user should
   see immediately.

### "label that decision right/wrong"

`node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js" label <hash> tp|fp` — see
"jev report" below for the full mechanics. Mention that
`~/.anti-hall/jev.json`'s `"audit": {"snippets": true}` must be turned on
BEFORE the decision was made for `label <hash>` (no verdict) to show the actual
judged text; without it, `label` still works (records tp/fp), it just can't
show the snippet for context.

### "turn <integration> on/off" / "promote <integration>"

See "Per-integration modes" below — `jev-setup.js mode <id> on|shadow|off`.
Before promoting `shadow` -> `on`, check that integration's row in `jev report`
for a KEEP suggestion first; promoting on thin data just adds a live behavior
change with no evidence behind it yet.

### Splitting the report by project or session

`--by project|session` runs the SAME report once per distinct value instead of
one combined table:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js" --by project
node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js" --by session
```

`--project <name>` filters to one project BEFORE reporting (combine with
`--json` for machine-readable output). `project` is a cwd-basename, never an
absolute path or repo URL (kept agnostic, same convention as everywhere else in
this codebase); `sessionId` is the Claude session id when the calling hook had
one to thread through (not every integration is session-scoped — e.g.
`devswarm-supervisor.js`'s background sweep never has one). Every integration
that DOES have a session (`speculation`, `claimLedger`, `newRequest`,
`mergeGateHedge`, `outputVerifyGuard`, `modelRouting`, `codexNudgeSubstantial`,
`tasklistTrivial`) now threads `sessionId` through on every call (v0.108.5 fix
— live `speculation` add-block rows used to log `sessionId: null` on every
row, making them unjoinable back to the transcript that produced them), plus
an optional `turnRef` (the transcript's last-line ISO timestamp, or a
line-count fallback) pointing at which turn the decision was about. A row
from BEFORE this feature existed, or from an integration that genuinely has
no session, groups under `unknown` — not an error, not dropped.
`recordOutcome()`'s `type:'outcome'` rows (e.g. `triage`'s answer-latency
join, `speculation`'s evidence-added/user-override outcomes) now carry
`project` the same way (v0.108.3 fix — they used to have none at all); an
outcome row logged before that fix still groups under `unknown` and cannot be
repaired retroactively (no source cwd to recover it from).

### Excluding a known-accidental run (v0.108.1) / reproducible windows (v0.108.5)

`--since <iso>` / `--until <iso>` bound the report to rows with `ts` inside that
window; `--exclude-window <iso>..<iso>` (repeatable) drops rows inside one
closed interval instead; `--exclude-project <name>` (repeatable) drops every raw row of a
project (e.g. a scratch clone that leaked rows; the daily rollups carry no project, so they
are unaffected). All of these apply BEFORE `--project`/`--by`/`--weekly`,
to both `jev-assist.ndjson` and `jev-triage.ndjson` (both live files AND every
rotated generation `.1` .. `.N`, `jev.logRotatedFiles`, default 10 — a window
spanning a rotation still sees every row). Days older than the oldest raw row
come from the daily rollups in `~/.anti-hall/logs/jev-daily/` (written before
each rotation) and print as a separate "Older history" section / `rollupHistory`
in `--json`; rollups carry no project/session, so `--project`/`--by` and
`--since`/`--until` do not filter them.
Use this when the user wants a bad/accidental run out of the numbers without
editing the log file directly:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js" \
  --exclude-window 2026-09-24T19:56:00Z..2026-09-24T22:23:00Z
```

Every report (default and `--by project|session`) now prints a `window: …`
line — the effective `since`/`until`/`exclude-window` bounds plus rows counted
vs excluded — right before the table, and the same data as a `window` object
in `--json` output. This is what makes two runs actually comparable: paste
both `window:` lines alongside the two agreement/changed-rate numbers so the
reader can confirm both analyses covered the same rows, not just "the same
flags happened to be typed."

### Weekly scorecard (automatic + on-demand)

`node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js" --weekly [--json]` — a
compact, ALWAYS-7-day summary: one line per integration with its current
`[mode]`, KEEP/REVIEW/REMOVE suggestion, a short reason, and call count. Same
thresholds as the full report, just condensed.

Separately, a SessionStart hook (`hooks/jev-weekly-scorecard.js`) checks this
automatically, AT MOST once every 7 days (a latch at
`~/.anti-hall/state/jev-weekly-notice.json` enforces the cadence — the check
runs at most once per week regardless of outcome, so a quiet week doesn't
trigger a re-check sooner), and — only in the interactive Primary/ordinary
session, never a DevSwarm child workspace's automated session — injects ONE
line when an integration has earned a KEEP or REMOVE verdict that its
`jev.json` mode has not caught up to yet:

```
Jev scorecard: modelRouting ready to switch ON — run /anti-hall:jev
```

This NEVER changes any mode itself — it is a pointer back to this skill (which
you'd then walk through as described in "how is Jev doing" above). Opt out
entirely with `"weeklyNotice": false` in `~/.anti-hall/jev.json` (default
`true` — opt-out, not opt-in). It is also silent whenever Jev itself is not
`enabled` at all.

### Shadow-review reminder (durable — "time to review the Jev shadow numbers")

Most integrations default to `shadow` (consulted + logged, never trusted).
The owner has said they forget to come back and check those numbers, so this
reminder is DURABLE plugin state (`~/.anti-hall/jev-review-state.json`), not a
per-session cron — a session cron dies with the session.

A review becomes DUE for a `shadow` integration once it has run in shadow for
`jev.reviewAfterDays` (default 7) AND logged at least `jev.reviewMinDecisions`
(default 30) decisions, unless it is currently snoozed or was already reviewed
within that same window. A SessionStart hook (`hooks/jev-review-reminder.js`,
main session only) and `doctor` both surface a due integration; the hook
injects one line like:

```
🔔 JEV REVIEW DUE: modelRouting (10d, 35 decisions). Tell the owner and offer
to run `/anti-hall:jev report` — they asked to be reminded.
```

When you see this line (or the owner otherwise brings it up), walk them
through `jev report` for that integration exactly as in "how is Jev doing"
above, help them decide promote (`mode <id> on`) / turn off (`mode <id> off`)
/ keep watching, and **once the owner has decided, run**:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" reviewed <integration>
```

This clears the reminder (and re-arms it `reviewAfterDays` after the next
review, so a shadow integration the owner keeps deferring on keeps coming
back). If the owner wants to defer without deciding yet, snooze it instead:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" snooze <integration> --days N
```

`review-due [--json]` lists what is currently due without changing anything —
useful to check state without waiting for the next SessionStart. Opt out of
the reminder (not the underlying tracking) with `"reviewReminder": false` /
`jev.reviewReminder` (default `true`).

### Two DIFFERENT "latency" numbers — do not conflate them

The per-integration table's `p50ms`/`p95ms` columns are the **Jev classifier
call's own latency** — how long the actual `POST /v1/systemone` request took.
For every integration except `triage` this comes from `jev-assist.ndjson`
decision rows' `ms` field; the `triage` row is sourced from
`jev-triage.ndjson`'s own classification rows' `ms` field instead (its real
decisions never land in `jev-assist.ndjson` — a separate file/schema, see
`hooks/lib/jev-triage.js` — so `buildReport` merges them in as their own
`triage` integration, `backend` always one of `jev`/`cache`/`baseline-only`,
never left `undefined`). Either way it is still **classifier latency**, NOT
reply turnaround. The separate **"triage answer-time"** section underneath the
table is a completely different thing: **agent reply turnaround** — how long
it took a Primary/child to reply to a message jev-triage.js labelled, from
`jev-triage.ndjson`'s `{type:"answered", latencyMs}` rows (`hooks/lib/jev-
triage.js`'s `recordAnswered` — these rows are excluded from the `triage`
integration row above by construction, they have no `hash` field). A real
classifier call is typically hundreds of milliseconds; a reply-turnaround
figure is typically minutes. If a p95 in seconds/minutes shows up anywhere
next to something claiming to be "Jev latency", that is the wrong number
attached to the wrong label — call it out.

### `agree%` denominator — distinct decisions, never raw rows

The `agree% (n=distinct)` column counts **distinct content-hash decisions**
with both a boolean `jev` answer and a caller-supplied `compare` signal, fresh
calls only (`backend !== 'cache'`) — the SAME dedupe discipline the
`changed%` column already applies. A cache-hit retry of the same decision no
longer adds its own extra vote: it used to (root cause of wildly inconsistent
agreement numbers across different windows/reports — a single popular,
disagreeing decision re-asked as a cache hit several times inflated its own
weight in the denominator each time). The printed `n=` is the real
denominator; read it alongside the percentage, not the percentage alone.

## "jev report"

`node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js" [--window 24h|7d]` — a
read-only per-integration summary (call volume, agreement %, decisions changed,
good-outcome rate, latency, estimated cost, real cost) with a KEEP/REVIEW/REMOVE
suggestion per integration. Never mutates state.

Each changed decision's content hash `h` doubles as its stable id. Label one as
a confirmed true/false positive:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js" label <hash> tp|fp
```

This is the ONLY write path `jev-report.js` has — it appends to a separate,
append-only `~/.anti-hall/logs/jev-labels.ndjson`, never touching
`jev-assist.ndjson`. A human label always wins over an AUTO label for the same
hash. AUTO labels are derived, at report time, from the SAME mechanical outcome
signal `recordOutcome()` already logs (e.g. the next main-thread turn citing a
file/tool after a Jev-added speculation block) — never re-parsing transcripts,
never treated as ground truth, and always reported separately from human
labels (`N TP (H human, A auto)`). Run `label <hash>` with no verdict to
inspect a hash read-only (current label + stored snippet, if any) without
writing anything.

### Audit snippets (opt-in, OFF by default)

Set `jev.audit.snippets` to true (`/anti-hall:settings`, or env
`ANTIHALL_JEV_AUDIT_SNIPPETS=1`; a legacy `jev.json` `{"audit": {"snippets": true}}`
still works) to store a small,
**redacted** local-only snippet for every decision that CHANGES an outcome, OR
that WOULD have changed the outcome had the integration's mode been `on`
(shadow mode is the default for every integration under evaluation, so this
covers the decisions you actually need to label — never for a call with no
change either way): the first ~200 characters of the text Jev judged, after
scrubbing common secret shapes (Bearer tokens, known key prefixes,
`key=`/`token=` assignments, emails, long base64/hex-looking runs). A
would-have-changed (shadow) row carries `shadow: true` so you can tell it
apart from an actual change. Stored separately, in
`~/.anti-hall/logs/jev-audit.ndjson`, file mode `600`, keyed by the decision's
hash. `label <hash>` prints it if one exists. Deletion is manual-only, never
automatic:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-report.js" prune-audit --days N
```

This is off by default precisely because it stores a piece of the actual
judged text (redacted, but still real content) — only turn it on if you want
that tradeoff for debugging/auditing precision labels.

The report also prints a one-line **headline** per integration combining
changed decisions, TP breakdown, cost efficiency ($/TP), latency, and the
KEEP/REVIEW/REMOVE suggestion, e.g.:

```
speculation: 6 changed/24h · 5 TP (3 human, 2 auto) · $0.02/TP · p50=120ms · KEEP
```

## Per-integration modes

`node "${CLAUDE_PLUGIN_ROOT}/scripts/jev-setup.js" mode <integration> on|shadow|off`
— `on` lets Jev actually influence that integration's outcome (bounded by its own
trust rule), `shadow` consults and logs Jev but never changes the result (useful to
gather `jev report` data before trusting it), `off` skips it entirely. `speculation`
and `triage` default to `on` once Jev is enabled (pre-existing behavior); every
other integration defaults to `shadow` until promoted.

**v0.108.4:** each of the integrations below (13 then; `postHandoverGate` added in v0.109.0) is also its own row/setting in the
`jevIntegrations` settings-schema section (`jevIntegrations.<id>`, e.g.
`jevIntegrations.modelRouting`) — its own table in `/anti-hall:settings`
(`node "${CLAUDE_PLUGIN_ROOT}/scripts/settings.js" show --section jevIntegrations`)
and its own row in Claude Code's native `/config` panel, titled
"Jev integration · <name>". `jev-setup.js mode` writes the canonical
`jevIntegrations.<id>` settings.json key (as well as `~/.anti-hall/jev.json` for
back-compat); a pre-existing `jev.json integrations.<id>` value, or a pre-0.108.4
settings.json `jev["integrations.<id>"]` value, keeps working and forward-migrates
into the new key automatically (see `companion/lib/migrations.js`
`migrateJevIntegrationsSection`) — nothing is ever deleted.

### All integrations

| id | judges | trust | default mode | Claude | Codex |
|---|---|---|---|---|---|
| `speculation` | is this claim unsupported speculation | `add-block` | `on` (legacy) | yes | yes |
| `triage` | mesh message urgency/kind label (own client, not this table's trust model) | n/a | `on` (legacy) | yes | yes |
| `modelRouting` | is this agent-spawn task actually mechanical | `relax-block` | `shadow` | yes | no — Codex has no `PreToolUse` Agent/Task-tool call to classify; subagent spawn is a separate `SubagentStart`/`SubagentStop` event with no pre-spawn `tool_input` |
| `claimLedger` | is a flagged claim genuinely unsupported by evidence | `relax-block` | `shadow` | yes | yes |
| `mergeGateHedge` | does this text hedge on merge-readiness | `relax-block` | `shadow` | yes | yes |
| `newRequest` | classify a prompt: new-request/follow-up/correction/question | `advisory` | `shadow` | yes | yes |
| `outputVerifyGuard` | does this test-runner output actually indicate a pass | `advisory` | `shadow` | yes | no — reads a `PostToolUse` Bash `tool_response` whose exact shape on Codex's shell tool is unverified (no captured payload); not wired until proven, to avoid a hook whose trigger never actually fires as intended |
| `gitGuardSelfCredit` | does this commit/PR message contain paraphrased AI self-credit | `add-block` **(never relaxes)** | `shadow` | yes | yes |
| `parentGateQuestion` | is this unread child message really a question awaiting reply | `add-block` (cache-only, zero network) | `shadow` | yes | yes |
| `tasklistTrivial` | is this session a genuinely non-trivial, multi-part effort | `relax-block` | `shadow` | yes | yes |
| `supervisorBlockerLabel` | is a stale child waiting-on-parent or genuinely wedged | `advisory` (cache-only, zero network) | `shadow` | yes | no — `companion/devswarm-supervisor.js` identity-binds to `claude --resume` processes specifically |
| `codexNudgeSubstantial` | are these file edits genuinely substantial (not just formatting) | `relax-block` | `shadow` | yes | no — nudges a Claude session to seek an independent Codex review; self-referential/meaningless inside a Codex session |
| `findingDedup` | do two deadly-loop TRIO findings describe the same underlying issue | `advisory` | `on` — 65/65 correct at confidence ≥0.85 on a 30-day, 3-project offline benchmark (see CHANGELOG 0.108.4) | yes | yes |
| `postHandoverGate` | does this new request fit in the remaining post-handover context budget | `advisory` | `off` — an offline benchmark (n=299) found park-recall 17.6% vs 28.8% for the agent's own size judgment plus the measured budget backstop, no gain over that baseline | yes | yes |
| `speculationFramed` | when a deterministic speculation-guard hedge hit sits under a FRAMED heading/line prefix (Expected/Plan/"Should be \<verb\>:"/Unverified/"not yet measured"), is it a stated expectation/plan or an unverified claim presented as fact | `relax-block` (Jev may only turn the framed hit's block into a non-block; an unframed hedge never consults Jev) | `shadow` | yes | yes |
| `dispatchTier` | how should each dispatchable task be dispatched: `workspace` / `workflow` / `subagent` (annotates the DISPATCH NOW line; the Primary makes the final call) | `advisory` (never blocks or forces) | `on` — advisory annotation only; asked `askDetached` when a task's text changes (PostToolUse TaskCreate/TaskUpdate); a no-workspaces repo (CLAUDE.md/AGENTS.md "no workspaces for real work", or `jev.dispatchTierNoWorkspaceRepos`) never gets `workspace`. Outcomes: followed/overridden, subagent one-lane vs escalated, workflow fan-out — `scripts/dispatch-report.js` | yes | n/a — Codex has no TaskCreate/Agent task tools, so there is nothing to annotate |
| `devswarmOnBrief` | is a child's off-scope work off its brief | recommendation (annotates a straying warning; never suppresses, blocks or kills) | `on` | yes | yes (shared hooks; burn input is Claude-transcript only) |
| `devswarmExtraSanctioned` | did the user ask for this off-scope work (threshold 0.9) | recommendation (annotates a straying warning; never suppresses, blocks or kills) | `on` | yes | yes (shared hooks; burn input is Claude-transcript only) |
| `devswarmWaitKind` | is an idle/stalled child stuck or waiting on CI/owner/peer | recommendation (annotates a straying warning; never suppresses, blocks or kills) | `on` | yes | yes (shared hooks; burn input is Claude-transcript only) |
| `devswarmLoop` | is a busy child looping on its step (threshold 0.9; burn figure as input) | recommendation (annotates a straying warning; never suppresses, blocks or kills) | `on` | yes | yes (shared hooks; burn input is Claude-transcript only) |
| `devswarmStepMap` | which step a summary without --step describes (threshold 0.8) | recommendation (annotates a straying warning; never suppresses, blocks or kills) | `on` | yes | yes (shared hooks; burn input is Claude-transcript only) |

`gitGuardSelfCredit`/`modelRouting`/`claimLedger`/`mergeGateHedge`/`tasklistTrivial`/
`codexNudgeSubstantial`/`findingDedup`/`postHandoverGate` use a real classifier call (`ask`/`askSync`/`askDetached`);
`parentGateQuestion`/`supervisorBlockerLabel` never make a network call at all —
both reuse an ALREADY-cached `hooks/lib/jev-triage.js` label populated by a
different surface that classified the same message earlier, so promoting either
to `on` is a config change only, no extra cost. `supervisorBlockerLabel`'s
sweep runs every ~90s but only asks/logs once per DISTINCT input (childId +
triage kind + ts) per workspace — a per-workspace state file under
`~/.anti-hall/devswarm/blocker-label-ask/` skips the repeat ask/log while the
input is unchanged, re-asking only when it changes or after
`devswarm.supervisorBlockerLabelReaskSec` (default 6h) elapses (v0.108.5 fix
— one static input used to produce 382 `jev-assist.ndjson` rows in 24h; a
follow-up fix made the persisted ask-state's `mode` actually round-trip
through `readBlockerLabelAskState` — it was written but never read back, so
every deduped/skipped sweep after the first ask in a re-ask window silently
reported no label instead of the still-valid one). `mergeGateHedge` uses `askDetached` (fire-and-forget): promoting it to `on` only
changes what gets LOGGED for now. `tasklistTrivial`/`codexNudgeSubstantial` log
fire-and-forget in shadow, but in `on` they ask synchronously (1.5 s cap,
fail-open to the nudge) and a confident "trivial" verdict skips the nudge.
`findingDedup` is called from `scripts/finding-dedup.js` (a standalone CLI, not a
hook), one async `ask()` call per candidate finding pair, concurrency 4, capped at
200 pairs per run — see the `deadly-loop`/`deadly-loop-multi` skills' Phase B3/step-5
wiring. See `docs/KB-jev-classifier.md` §10 for the full table
with hook/event/API details.

## Never do this

- Never print, log, echo, or commit the key — not in this chat, not in a file,
  not in a script argument.
- Never guess which provider a key belongs to from its shape — always ask.
- Where a stored key is visible: a key saved through the plugin options reaches hooks and the workers they spawn
  only. `jev-setup test`/`status`, `jev-report` and `finding-dedup` run from a shell see no plugin option and
  print a one-line "no Jev key visible to this process" reason; enabling `jev.allowLegacyKeyRead`
  (home settings only, `--confirmed`; env cannot set it) with a key file makes the key available to them.
- Preferred: the user stores the key in the plugin's options screen (Claude Code prompts for these options when the plugin is enabled; they can be changed later from the plugin's configuration) (anti-hall -> `jev_vercel_api_key` or `jev_typesafe_api_key`, one per vendor). The key file
  that the jev-setup script's set-key command writes is read only when
  `jev.allowLegacyKeyRead` is on; never store the key anywhere else.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.