# pi-classifier

System One decision models ([TypeSafe Jev](https://docs.typesafe.ai/)) for
[Pi](https://pi.dev). Jev returns typed answers with calibrated probabilities —
never text — so this extension surfaces it as a **tool**, not a model.

Two pieces:

1. **`classify` tool** — the agent sends `{state, questions}`, gets typed
   answers: `noul` (P(yes)), `choice` (option + probabilities + confidence),
   `score` (weighted position + confidence). Use for routing, verification,
   and gating decisions.
2. **Permission auto-approve hook (on by default; explicit opt-out wins)** — shell
   commands Jev is confident are *reversible* and *serve the task* run without
   prompting; set `permission.enabled: false` to turn it off. [The OpenRouter
   cookbook pattern](https://openrouter.ai/docs/cookbook/coding-agents/auto-approve-permission-prompts-with-jev).

## Install

```bash
pi install @bacnh85/pi-classifier
```

## Configure

Run **`/classifier-config`** in Pi (TUI): an arrow-key panel for the baseUrl,
the decision model — with completions pulled live from the router's
`GET /v1/systemone/models` (yardmaster) — and the permission block below.
`/classifier-config show` prints the config plus discovered models in any
mode. Everything is still plain JSON in global settings
(`~/.pi/agent/settings.json`) — never repo scope, because
the endpoint receives your API key as Bearer:

```jsonc
{
  "classifier": {
    "baseUrl": "http://localhost:8787/v1",  // yardmaster (or https://openrouter.ai/api)
    "model": "jev/jev-latest",              // id the upstream knows: jev/jev-latest, or/typesafe/jev-1.13, jev-latest…
    "permission": {                          // default ON/enforce — auto-approves reversible,
      "enabled": true,                       // task-serving commands; set enabled:false to opt out
      "mode": "enforce",                     // "observe" logs decisions without acting
      "threshold": 0.9                       // both nouls must clear it
    },
    "planGate": {                            // opt-in — default OFF; used by pi-plan
      "enabled": true,
      "mode": "observe",                     // start here; "enforce" to act
      "threshold": 0.9                       // independent of permission.threshold
    }
  }
}
```

API key: `CLASSIFIER_API_KEY` env, or the `classifier` credential in
`~/.pi/agent/auth.json`:

```json
{ "classifier": { "key": "ar-..." } }
```

Through [yardmaster](https://github.com/bacnh85/yardmaster): point `baseUrl`
at the router's `/v1`, set `model` to the prefixed id (`jev/jev-latest` for
the TypeSafe-direct provider, `or/typesafe/jev-1.13` via OpenRouter) and use
your router key. Pricing: $0.042/Mtok input, output free. The panel's model
completions come from yardmaster's decision-model listing
(`GET /v1/systemone/models`); routers without it (OpenRouter direct,
TypeSafe direct) just fall back to manual entry.

## The safety envelope

Non-negotiables, in order:

1. **Static risky list first.** `rm -rf`, `sudo`, force-push/hard-reset,
   pipe-to-shell, publish/deploy CLIs, credential paths → never sent to Jev,
   never auto-approved. The list is deliberately short and shallow.
2. **Never auto-denies.** Any outcome other than a confident yes (low score,
   timeout, 4xx/5xx, malformed answer, missing key, no UI) falls back to the
   normal prompt. Worst case is one extra prompt, never an unwanted command.
3. **Observe mode (opt-in).** Enforce is the default posture; set
   `permission.mode: "observe"` to log the decision it *would* have made to
   `~/.pi/agent/classifier.log` without acting. Run it for a few days, read
   the log, then flip `mode: "enforce"`.
4. **One audit line per decision** — command, scores, elapsed ms, model.
5. **Compound commands are split** on operators before the risky check; a
   separator hidden inside quotes can only add a prompt, never hide a command.

Host deny rules always win: pi-classifier only ever *allows*; it cannot
override an explicit deny from pi-permission or the harness.

## Plan gate (pi-plan integration)

`planGate` powers pi-plan's plan-mode confirm tier: when a bash command lands
in the "confirm" tier during plan mode, pi-plan asks Jev *"is this read-only
and needed for planning?"* and auto-allows only a confident yes. Jev may only
**reduce prompts** — it can never unlock a write (the outer gate blocks those
before the gate runs), never deny (every non-confident outcome falls through
to the normal prompt), and every verdict is audited to `classifier.log` with
`source: "plan-gate"`.

Workflow: set `classifier.planGate.enabled: true` → observe is the default
(logs would-be allows, still prompts) → review the log → flip
`mode: "enforce"`. Risky-list commands (`rm -rf`, pipe-to-shell, …) never
reach Jev. The library export `planGateVerdict(opts, command, cwd, task)` is
what pi-plan calls; it is exported for tests and library hosts.

## Verification cache

Verdicts are cached (LRU, 100 entries) — permission-hook keys are
`command\0cwd\0task` (task truncated to 200 chars, since 0.2.1), plan-gate
keys are `plan\0command\0cwd` (task excluded — plan commands are generic) —
so repeated `bun test` doesn't re-pay Jev or add latency every time.

## Using the classify tool directly

Ask the agent: *"Classify this ticket: is_urgent noul, team choice
(billing/technical/account), severity score 0-3"* — it composes the questions
and returns the typed answers. Questions run in parallel inside one request;
probabilities jitter ±0.08 between identical calls, so thresholds are policy —
tune them on your own traffic.

## Coexistence with pi-permission

Both observe `tool_call`. pi-classifier's silent-allow and pi-permission's
`ask` compose by load order: whichever extension returns first wins. If you
run both, the intended stack is pi-permission `deny` rules (they win over
everything) + pi-classifier enforce for the confident middle. Test the
combination before trusting it.
