pi-jev-auto-mode 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,11 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - **The middle band no longer asks the user by default.** An auto mode that stops to ask has
6
+ handed the decision back to a human, and the agent can always ask in conversation if it needs
7
+ guidance. A judgment that is neither satisfied nor rejected now blocks, so the gate never
8
+ takes over the screen.
9
+ - `uncertain` setting and `/jev-auto-mode uncertain deny|ask|allow` control it. `deny` is the
10
+ default; `ask` restores the confirmation dialog; `allow` trusts the band.
11
+ - `/jev-auto-mode threshold edit` picks a rule and prompts for a value, showing each rule's
12
+ current threshold next to the last probability the model returned for it.
13
+
14
+ ## 0.1.2
15
+
16
+ - Fix the screen thrashing that happened whenever a judgment was delegated to the user: the
17
+ confirmation dialog was handed the whole command, and Pi's dialogs do not clip their content,
18
+ so a long command produced a dialog taller than the terminal. The dialog now shows a bounded
19
+ preview and says what was hidden.
20
+
21
+ ## 0.1.1
22
+
23
+ - Correct the product name. It is **Jev** — TypeSafe's System One model, spelled with a
24
+ capital J and lowercase `ev`, as in their announcement, FAQ, and the model id
25
+ `typesafe-ai/jev` — not "JEV". Fixed in the README, docs, source comments, and the package
26
+ description. The `0.1.0` metadata cannot be edited, so this release exists to carry the
27
+ corrected name. No behavioral change.
28
+
3
29
  ## 0.1.0
4
30
 
5
31
  Initial release.
6
32
 
7
33
  - Two-layer gate: a deterministic policy envelope (hard-deny, allow/deny patterns,
8
- dangerous-command detection, protected paths) and a JEV semantic layer that only sees
34
+ dangerous-command detection, protected paths) and a Jev semantic layer that only sees
9
35
  what the first layer escalated.
10
36
  - Conditions carry a `mode` (`required` / `hazard`) and a `severity` (`hazard` / `soft`),
11
37
  calibrated against measured probabilities from the real API (`docs/calibration.md`).
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # pi-jev-auto-mode
2
2
 
3
3
  Auto mode for the [Pi coding agent](https://github.com/earendil-works/pi-mono) backed by
4
- **JEV** (TypeSafe System One, a decision-only model). Pi has no built-in permission system,
4
+ **Jev** (TypeSafe System One, a decision-only model). Pi has no built-in permission system,
5
5
  so a gate either exists as an extension or it does not exist at all. This one judges
6
6
  `bash`, `write`, and `edit` tool calls semantically and **fails closed** whenever a decision
7
7
  cannot be made.
8
8
 
9
- > **Status: milestones 1–3 are complete.** The deterministic envelope, the JEV engine,
9
+ > **Status: milestones 1–3 are complete.** The deterministic envelope, the Jev engine,
10
10
  > real-API calibration, settings, policy notes, per-rule threshold tuning, and decision
11
- > records are implemented and tested (157 tests, no network). See [`docs/design.md`](./docs/design.md) for
11
+ > records are implemented and tested (171 tests, no network). See [`docs/design.md`](./docs/design.md) for
12
12
  > the roadmap and [`docs/calibration.md`](./docs/calibration.md) for the measured
13
13
  > probabilities behind every threshold.
14
14
 
@@ -18,22 +18,22 @@ The gate has two layers, in this order:
18
18
 
19
19
  1. **Deterministic policy** — hard-deny commands, your allow/deny patterns, dangerous-command
20
20
  detection, protected paths. Hard-deny is never handed to the semantic layer.
21
- 2. **Semantic judgment (JEV)** — only the calls the first layer escalated.
21
+ 2. **Semantic judgment (Jev)** — only the calls the first layer escalated.
22
22
 
23
23
  ```
24
- hard-deny → block (never reaches JEV)
24
+ hard-deny → block (never reaches Jev)
25
25
  your deny pattern → block
26
26
  your allow pattern → allow
27
27
  safe read-only command → run, no record
28
28
  in-project write/edit → run, no record
29
- everything else → JEV: allow · block · confirm · block-if-undecidable
29
+ everything else → Jev: allow · block · block-if-undecidable
30
30
  ```
31
31
 
32
32
  `rm -rf build` inside the repository is recognized as a scoped local deletion. A write to
33
33
  `.env`, `.git/`, `~/.ssh`, `.pi/`, `.github/workflows/`, or `AGENTS.md` is escalated even when
34
34
  the path is inside the working directory.
35
35
 
36
- ### How JEV decides
36
+ ### How Jev decides
37
37
 
38
38
  Conditions are phrased so the safe state is "yes", and each one is classified by
39
39
  `mode` and `severity`:
@@ -48,8 +48,13 @@ Conditions are phrased so the safe state is "yes", and each one is classified by
48
48
  So `intent_coverage` ("is this what the user asked for?") is the permission question, and
49
49
  questions like "is a secret being sent to a network endpoint" are hazard detectors that only
50
50
  block when they are sure. Posting a private key is never cleared by intent; force-pushing a
51
- feature branch the user asked for is. Everything that cannot be decided — no engine, timeout,
52
- malformed response, cancellation — blocks.
51
+ feature branch the user asked for is.
52
+
53
+ **Nothing is delegated to the user by default.** The middle band — where Jev is neither
54
+ satisfied nor rejecting — resolves to a block, so Jev's probability is the whole answer and the
55
+ gate never takes over the screen. `/jev-auto-mode uncertain ask` restores the confirmation
56
+ dialog if you want it; `allow` trusts the band. Everything else that cannot be decided — no
57
+ engine, timeout, malformed response, cancellation — also blocks.
53
58
 
54
59
  ## Install
55
60
 
@@ -84,14 +89,17 @@ Packages are discovered in the [package gallery](https://pi.dev/packages) throug
84
89
  /jev-auto-mode policy clear
85
90
  /jev-auto-mode threshold show thresholds and the last observed probability per rule
86
91
  /jev-auto-mode threshold <rule> <0.5-1> set one threshold
92
+ /jev-auto-mode threshold edit pick a rule and type a value
87
93
  /jev-auto-mode threshold reset [rule] restore the calibrated default
94
+ /jev-auto-mode uncertain show what the middle band resolves to
95
+ /jev-auto-mode uncertain deny|ask|allow
88
96
  ```
89
97
 
90
98
  ```
91
99
  pi --jev-auto-mode start with auto mode enabled
92
100
  ```
93
101
 
94
- The semantic layer needs a [TypeSafe](https://typesafe.ai/) API key. JEV is early access, so an
102
+ The semantic layer needs a [TypeSafe](https://typesafe.ai/) API key. Jev is early access, so an
95
103
  account may be waitlisted; **the gate still works without one**, running in ask-only mode
96
104
  (confirm in a UI, block without one) rather than silently allowing everything.
97
105
 
@@ -164,6 +172,7 @@ Policy notes: `$PI_CODING_AGENT_DIR/jev-auto-mode-policy.md`.
164
172
  "disallowedCommands": ["npm publish*"],
165
173
  "extraProtectedPaths": [],
166
174
  "maxStateCharacters": 120000,
175
+ "uncertain": "deny",
167
176
  "thresholds": {}
168
177
  }
169
178
  ```
@@ -231,10 +240,10 @@ Layout:
231
240
  The deterministic pattern catalogue is adapted from
232
241
  [`@nilskluewer/pi-auto-permission-gate`](https://github.com/nilskluewer/pi-auto-permission-gate)
233
242
  (MIT), and the three-layer structure (fast paths → hard rules → classifier) follows the same
234
- extension and Qwen Code's Auto Mode. The JEV design constraints (fail closed, two-sided
243
+ extension and Qwen Code's Auto Mode. The Jev design constraints (fail closed, two-sided
235
244
  thresholds that keep the middle band meaningful, one request per judgment) come from measuring
236
245
  the API directly — [`docs/calibration.md`](./docs/calibration.md) records the measurements and
237
- the reasoning. Nothing here depends on a wrapper library: the JEV layer is written against the
246
+ the reasoning. Nothing here depends on a wrapper library: the Jev layer is written against the
238
247
  official SDK.
239
248
 
240
249
  ## License
package/SECURITY.md CHANGED
@@ -6,7 +6,7 @@ This extension stands between a coding agent and your shell, so its own failure
6
6
 
7
7
  Open a private security advisory on the repository, or an issue if the report does not need to
8
8
  stay private. Please include the tool call, the recorded decision (`/jev-auto-mode` records
9
- are expandable in the transcript), and the JEV probabilities if you have them.
9
+ are expandable in the transcript), and the Jev probabilities if you have them.
10
10
 
11
11
  ## What is in scope
12
12
 
@@ -23,7 +23,7 @@ The gate is two layers and the order is the enforcement:
23
23
  1. A deterministic envelope (hard-deny, user rules, dangerous-command patterns, protected
24
24
  paths). Hard-deny returns before the semantic layer is constructed, so a probabilistic
25
25
  verdict can never resurrect it.
26
- 2. JEV, which only sees calls the first layer escalated.
26
+ 2. Jev, which only sees calls the first layer escalated.
27
27
 
28
28
  Anything that cannot be decided — no key, timeout, malformed response, a response missing an
29
29
  answer, cancellation, an oversized request — blocks. See
@@ -18,7 +18,7 @@ Thirteen fixtures, one request each (6–7 `noul` questions per request), first
18
18
  2026-09-17 against `jev-latest`. Every condition's probability is printed, not just
19
19
  the failing ones, because a threshold cannot be chosen without the passing values.
20
20
 
21
- > **Caveat.** This is one sample per fixture. JEV is probabilistic: reruns moved
21
+ > **Caveat.** This is one sample per fixture. Jev is probabilistic: reruns moved
22
22
  > some conditions by ±0.05 (for example `local_scope` on the pytest fixture was
23
23
  > 0.78 and 0.81 across runs, `path_not_protected` on the `.env` fixture 0.28 and
24
24
  > 0.25). Treat the numbers as evidence about *bands*, not as exact values, and
package/docs/design.md CHANGED
@@ -16,11 +16,11 @@ Two prior arts shaped the structure:
16
16
  | [`@nilskluewer/pi-auto-permission-gate`](https://github.com/nilskluewer/pi-auto-permission-gate) (Pi extension) | hard-deny → user rules → classifier → confirmation → no-UI block; classifier failure is fail-closed; decisions recorded with `pi.appendEntry` (out of LLM context); user policy as a Markdown note; allow patterns disabled for shell control syntax |
17
17
  | Qwen Code Auto Mode | three layers: deterministic fast paths and allow rules first, then a classifier; protected "persistence surfaces" (`package.json`, `.github/workflows/`, agent config) always go through the classifier even when the target is inside the workspace |
18
18
 
19
- What is different here: the classifier is **JEV**, a decision-only model (unstructured state in,
19
+ What is different here: the classifier is **Jev**, a decision-only model (unstructured state in,
20
20
  typed decisions out). The call is cheap, has no tokens to inject through, and returns calibrated
21
21
  probabilities rather than prose that has to be parsed.
22
22
 
23
- JEV facts this design leans on:
23
+ Jev facts this design leans on:
24
24
 
25
25
  - One request carries many `noul` questions; they are evaluated **in parallel and
26
26
  independently**, so adding questions barely changes latency.
@@ -35,17 +35,18 @@ JEV facts this design leans on:
35
35
  ```
36
36
  tool_call(bash | write | edit)
37
37
  ├─ 0. auto mode off / tool not gated → pass through
38
- ├─ 1. hard-deny (deterministic) → block, no JEV
39
- ├─ 2. user disallow pattern → block, no JEVJEV never sees these
38
+ ├─ 1. hard-deny (deterministic) → block, no Jev
39
+ ├─ 2. user disallow pattern → block, no JevJev never sees these
40
40
  ├─ 3. user allow pattern → allow (recorded) ┘
41
41
  ├─ 4. read-only command, or user-declared safe command
42
42
  │ → pass through (silent)
43
43
  ├─ 5. in-project write/edit, unprotected → pass through (silent)
44
- └─ 6. JEV: one request, all conditions
44
+ └─ 6. Jev: one request, all conditions
45
45
  ├ every `required` condition satisfied, no hazard rejected → allow
46
46
  ├ any `hazard` condition rejected → block
47
47
  ├ any `soft` condition rejected → block, unless the user's own request covers it
48
- ├ any `required` condition unclear → confirm in a UI, block without one
48
+ ├ any `required` condition unclear → resolved by the `uncertain` setting
49
+ │ (default: block; `ask` prompts, `allow` passes)
49
50
  └ unavailable → block (fail-closed)
50
51
  └─ 7. record the decision via appendEntry (never enters LLM context)
51
52
  ```
@@ -76,7 +77,13 @@ participates:
76
77
 
77
78
  Composition happens in code, not in the model: one rejection from a `hazard`-severity condition
78
79
  blocks, a `soft` rejection is cleared by a satisfied `intent_coverage`, an unclear `required`
79
- condition escalates, otherwise the call is approved. One question, one judgment; no compound
80
+ condition is resolved by the `uncertain` setting, otherwise the call is approved.
81
+
82
+ The default for that resolution is `deny`. Handing an unclear judgment to the user is what a
83
+ non-auto mode does, and it makes the gate a source of interruptions; the agent can ask in
84
+ conversation if it needs guidance. The `ask` path still exists, and when it is used the dialog
85
+ shows a bounded preview — Pi's dialogs do not clip their content, so an unbounded command
86
+ produces a dialog taller than the terminal. One question, one judgment; no compound
80
87
  questions, and the model never has to weigh concerns against each other.
81
88
 
82
89
  `intent_coverage` is the only permission question. It reads user-authored messages only — never
@@ -98,14 +105,14 @@ shipped to everyone.
98
105
 
99
106
  ## Tests
100
107
 
101
- 157 tests, none of which need a network or an API key: the engine and transport are stubbed so
108
+ 171 tests, none of which need a network or an API key: the engine and transport are stubbed so
102
109
  every branch — allow, deny, cleared-by-intent, uncertain, each unavailable reason, boundary
103
110
  probabilities — is deterministic. The real API is exercised by two scripts that are not part of
104
111
  the published package:
105
112
 
106
113
  - `scripts/calibrate.ts` sends the fixture set and prints every condition's probability.
107
114
  - `scripts/e2e.ts` runs the same fixtures through the real gate path (deterministic layer,
108
- real JEV, block/ask routing, records) and compares the decision against the expectation.
115
+ real Jev, block/ask routing, records) and compares the decision against the expectation.
109
116
 
110
117
  ## Remaining work
111
118
 
package/docs/security.md CHANGED
@@ -60,7 +60,9 @@ Everything below resolves to **block**. Silence is never consent.
60
60
  | State + questions over the shared budget | block (`state_too_large`) before the request is sent |
61
61
  | Engine throws | block (`engine_error`) |
62
62
  | Request cancelled (Esc) | block |
63
- | No UI available for a confirmation | block (`no-ui`) |
63
+ | A middle-band judgment, with the default `uncertain: deny` | block (`uncertain`) |
64
+ | A middle-band judgment with `uncertain: allow` | allow — an explicit choice to trust the band |
65
+ | No UI available for a confirmation when `uncertain: ask` | block (`no-ui`) |
64
66
  | A condition answered by fewer than all keys | block (`malformed_response`) — a missing answer is never an approval |
65
67
 
66
68
  A confirmation is not a bypass: it runs only when the semantic layer said `uncertain`, never
@@ -76,7 +78,7 @@ when it said `deny` or when no decision was available.
76
78
  paths: a write there changes what the agent believes it was told.
77
79
  - One dedicated condition (`prompt_injection_absent`) asks whether the call is trying to make
78
80
  other state act as instructions.
79
- - The gate never returns a JEV rationale verbatim as a system-level instruction; a block
81
+ - The gate never returns a Jev rationale verbatim as a system-level instruction; a block
80
82
  reason is a tool-call error string, which is the weakest channel it can use.
81
83
 
82
84
  ## What is sent to TypeSafe
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-jev-auto-mode",
3
- "version": "0.1.0",
4
- "description": "JEV (TypeSafe System One) backed auto mode for the Pi coding agent: semantically auto-approves bash, write, and edit tool calls and fails closed when a decision cannot be made.",
3
+ "version": "0.2.0",
4
+ "description": "Jev (TypeSafe System One) backed auto mode for the Pi coding agent: semantically auto-approves bash, write, and edit tool calls and fails closed when a decision cannot be made.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",
package/src/call.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Turn a Pi tool call into the shape JEV will judge.
2
+ * Turn a Pi tool call into the shape Jev will judge.
3
3
  *
4
4
  * What leaves this module is what a third party (TypeSafe) gets to see, so the
5
5
  * boundary is explicit: the command text and the target path are sent, file
@@ -143,7 +143,7 @@ export const NO_POLICY_PLACEHOLDER = "(no user policy configured)";
143
143
  export const NO_INTENT_PLACEHOLDER = "(no recent user message available)";
144
144
 
145
145
  /**
146
- * Assemble the JEV request state.
146
+ * Assemble the Jev request state.
147
147
  *
148
148
  * The user policy is `context` (session-scoped) so conditions can name it, and
149
149
  * the call plus the user intent are `value` (per-call).
package/src/decide.ts CHANGED
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * The gate itself (ordering, blocking, recording) is deterministic. Everything
5
5
  * probabilistic sits behind `DecisionEngine`, so the extension can be tested
6
- * without a network and so the JEV implementation can be swapped or disabled
6
+ * without a network and so the Jev implementation can be swapped or disabled
7
7
  * without touching the safety-critical path.
8
8
  */
9
9
 
10
10
  import type { GatedCall, RepoFacts } from "./call.ts";
11
11
 
12
- export type DecisionSource = "hard-deny" | "user-rule" | "engine" | "unavailable" | "no-ui" | "user";
12
+ export type DecisionSource = "hard-deny" | "user-rule" | "engine" | "uncertain" | "unavailable" | "no-ui" | "user";
13
13
 
14
14
  /**
15
15
  * A display-ready report of one condition's judgment.
package/src/extension.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * JEV auto mode for the Pi coding agent.
2
+ * Jev auto mode for the Pi coding agent.
3
3
  *
4
4
  * The gate has two layers, and the order matters:
5
5
  *
6
6
  * 1. A deterministic policy layer (hard-deny, user rules, dangerous-pattern
7
7
  * detection, protected paths). Hard-deny is not negotiable.
8
- * 2. A semantic layer (JEV) that only ever sees calls the deterministic layer
8
+ * 2. A semantic layer (Jev) that only ever sees calls the deterministic layer
9
9
  * decided to escalate, and whose "allow" can never resurrect a hard-denied
10
10
  * call.
11
11
  *
@@ -51,9 +51,18 @@ import {
51
51
  type DecisionRecord,
52
52
  type DecisionRecorder,
53
53
  } from "./records.ts";
54
- import { DEFAULT_SETTINGS, JevAutoModeStore, parseThreshold, type JevAutoModeSettings, type SettingsScope } from "./settings.ts";
55
54
  import {
55
+ DEFAULT_SETTINGS,
56
+ JevAutoModeStore,
57
+ isUncertainAction,
58
+ parseThreshold,
59
+ type JevAutoModeSettings,
60
+ type SettingsScope,
61
+ } from "./settings.ts";
62
+ import {
63
+ buildConfirmationDialog,
56
64
  describeSettings,
65
+ UNCERTAIN_EXPLANATION,
57
66
  formatRuleTable,
58
67
  POLICY_HEADER,
59
68
  statusText,
@@ -171,7 +180,7 @@ function permit(deps: DecisionDeps, input: RecordInput): undefined {
171
180
 
172
181
  /** Wraps an engine rationale so the model gets an actionable reason, not a verdict. */
173
182
  function blockReason(rationale: string): string {
174
- return `JEV auto mode blocked this tool call. ${rationale} Do not repeat the same call unchanged; change the approach or ask the user.`;
183
+ return `Jev auto mode blocked this tool call. ${rationale} Do not repeat the same call unchanged; change the approach or ask the user.`;
175
184
  }
176
185
 
177
186
  /**
@@ -308,6 +317,32 @@ export async function evaluateToolCall(
308
317
  }
309
318
 
310
319
  case "uncertain": {
320
+ // The middle band is a policy decision, not a prompt by default. An auto mode
321
+ // that stops to ask the user has handed the decision back to the human, and
322
+ // the agent can always ask in conversation if it needs guidance.
323
+ if (state.settings.uncertain === "deny") {
324
+ const rationale = `No condition decided the call, and the uncertain band is resolved to a block. ${verdict.rationale}`;
325
+ return blocked(deps, {
326
+ call,
327
+ reasons,
328
+ status: "blocked",
329
+ source: "uncertain",
330
+ rationale,
331
+ evidence,
332
+ });
333
+ }
334
+
335
+ if (state.settings.uncertain === "allow") {
336
+ return permit(deps, {
337
+ call,
338
+ reasons,
339
+ status: "allowed",
340
+ source: "uncertain",
341
+ rationale: `No condition was violated and the uncertain band is configured to allow. ${verdict.rationale}`,
342
+ evidence,
343
+ });
344
+ }
345
+
311
346
  if (!ctx.hasUI) {
312
347
  const rationale = `${verdict.rationale} No UI is available to confirm, so the call was blocked.`;
313
348
  return blocked(
@@ -317,16 +352,13 @@ export async function evaluateToolCall(
317
352
  );
318
353
  }
319
354
 
320
- const dialog = [
321
- "JEV auto mode wants confirmation before this runs.",
322
- "",
323
- `Tool: ${call.tool}`,
324
- ...(call.command ? [call.command] : []),
325
- ...(call.path ? [call.path] : []),
326
- "",
327
- `Matched: ${reasons.join(", ")}`,
328
- `Rationale: ${verdict.rationale}`,
329
- ].join("\n");
355
+ const dialog = buildConfirmationDialog({
356
+ tool: call.tool,
357
+ ...(call.command === undefined ? {} : { command: call.command }),
358
+ ...(call.path === undefined ? {} : { path: call.path }),
359
+ reasons,
360
+ rationale: verdict.rationale,
361
+ });
330
362
 
331
363
  const choice = await ctx.ui.select(dialog, ["No", "Yes"]);
332
364
  if (choice !== "Yes") {
@@ -338,7 +370,7 @@ export async function evaluateToolCall(
338
370
  rationale: "The user declined the confirmation.",
339
371
  evidence,
340
372
  });
341
- return { block: true, reason: "Blocked by the user at the JEV auto mode confirmation." };
373
+ return { block: true, reason: "Blocked by the user at the Jev auto mode confirmation." };
342
374
  }
343
375
 
344
376
  return permit(deps, {
@@ -353,6 +385,47 @@ export async function evaluateToolCall(
353
385
  }
354
386
  }
355
387
 
388
+ /**
389
+ * Pick a rule, then type a value.
390
+ *
391
+ * The direct form (`threshold <rule> <value>`) is faster once the rule ids are
392
+ * known; this exists so tuning does not require remembering them.
393
+ */
394
+ async function editThreshold(
395
+ ctx: { ui: GateUi },
396
+ state: GateState,
397
+ observed: ReadonlyMap<string, ObservedCondition>,
398
+ save: (ctx: GateContext) => Promise<void>,
399
+ rebuild: () => Promise<void>,
400
+ ): Promise<void> {
401
+ const choices = DEFAULT_RULES.map((rule) => {
402
+ const threshold = state.settings.thresholds[rule.id] ?? rule.threshold;
403
+ const last = observed.get(rule.id);
404
+ return `${rule.id} (t=${threshold}${last ? `, last p=${last.probability.toFixed(2)}` : ""})`;
405
+ });
406
+
407
+ const picked = await ctx.ui.select("Which condition?", choices);
408
+ if (picked === undefined) return;
409
+ const ruleId = picked.split(" ")[0] ?? "";
410
+ const rule = ruleById(ruleId);
411
+ if (!rule) return;
412
+
413
+ const current = state.settings.thresholds[ruleId] ?? rule.threshold;
414
+ const entered = await ctx.ui.input(`${ruleId}: threshold (0.5-1.0, default ${rule.threshold})`, String(current));
415
+ if (entered === undefined) return;
416
+
417
+ const threshold = parseThreshold(Number(entered.trim()));
418
+ if (threshold === undefined) {
419
+ ctx.ui.notify(`A threshold must be greater than 0.5 and at most 1.0 (got \`${entered.trim()}\`).`, "error");
420
+ return;
421
+ }
422
+
423
+ state.settings = { ...state.settings, thresholds: { ...state.settings.thresholds, [ruleId]: threshold } };
424
+ await save(ctx as unknown as GateContext);
425
+ await rebuild();
426
+ ctx.ui.notify(`\`${ruleId}\` now requires p >= ${threshold}`, "info");
427
+ }
428
+
356
429
  export interface RegisterOptions {
357
430
  /** Override the engine (tests, or a different judgment backend). */
358
431
  readonly engine?: DecisionEngine;
@@ -450,7 +523,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
450
523
  };
451
524
 
452
525
  pi.registerFlag(AUTO_MODE_FLAG, {
453
- description: "Start with JEV auto mode enabled",
526
+ description: "Start with Jev auto mode enabled",
454
527
  type: "boolean",
455
528
  default: false,
456
529
  });
@@ -458,7 +531,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
458
531
  registerDecisionEntryRenderer(pi);
459
532
 
460
533
  pi.registerCommand(AUTO_MODE_COMMAND, {
461
- description: "Show or change the JEV auto mode settings",
534
+ description: "Show or change the Jev auto mode settings",
462
535
  getArgumentCompletions: (argumentPrefix) => {
463
536
  const value = String(argumentPrefix ?? "");
464
537
  const tokens = value.split(/\s+/).filter(Boolean);
@@ -513,7 +586,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
513
586
  engineId: deps.engine.id,
514
587
  scope: state.scope,
515
588
  });
516
- ctx.ui.notify(`JEV auto mode ${value === "on" ? "enabled" : "disabled"}.`, "info");
589
+ ctx.ui.notify(`Jev auto mode ${value === "on" ? "enabled" : "disabled"}.`, "info");
517
590
  return;
518
591
  }
519
592
 
@@ -523,7 +596,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
523
596
  }
524
597
 
525
598
  if (value === "policy edit") {
526
- const edited = await ctx.ui.editor("JEV auto mode policy", state.policyNotes || POLICY_HEADER);
599
+ const edited = await ctx.ui.editor("Jev auto mode policy", state.policyNotes || POLICY_HEADER);
527
600
  if (edited === undefined) return;
528
601
  await store.savePolicyNotes(edited);
529
602
  state.policyNotes = await store.loadPolicyNotes();
@@ -533,7 +606,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
533
606
 
534
607
  if (value === "policy clear") {
535
608
  const confirmed = await ctx.ui.confirm(
536
- "Clear JEV auto mode policy notes?",
609
+ "Clear Jev auto mode policy notes?",
537
610
  "The semantic layer will fall back to its built-in criteria.",
538
611
  );
539
612
  if (!confirmed) return;
@@ -616,6 +689,28 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
616
689
  return;
617
690
  }
618
691
 
692
+ if (value === "threshold edit") {
693
+ await editThreshold(ctx, state, observed, save, rebuildEngine);
694
+ return;
695
+ }
696
+
697
+ if (value.startsWith("uncertain")) {
698
+ const argument = value.slice("uncertain".length).trim();
699
+ if (argument === "") {
700
+ ctx.ui.notify(`uncertain: ${state.settings.uncertain}\n\n${UNCERTAIN_EXPLANATION}`, "info");
701
+ return;
702
+ }
703
+ if (!isUncertainAction(argument)) {
704
+ ctx.ui.notify(`Expected one of deny, ask, allow.\n\n${UNCERTAIN_EXPLANATION}`, "error");
705
+ return;
706
+ }
707
+
708
+ state.settings = { ...state.settings, uncertain: argument };
709
+ await save(gateContext);
710
+ ctx.ui.notify(`uncertain band now resolves to: ${argument}\n\n${UNCERTAIN_EXPLANATION}`, "info");
711
+ return;
712
+ }
713
+
619
714
  const thresholdMatch = /^threshold\s+(\S+)(?:\s+(\S+))?$/.exec(value);
620
715
  if (thresholdMatch) {
621
716
  const ruleId = thresholdMatch[1] ?? "";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared noul criteria.
3
3
  *
4
- * The default criteria matter more than they look. JEV is calibrated, and
4
+ * The default criteria matter more than they look. Jev is calibrated, and
5
5
  * `noul` has no `confidence` field: the only signal is the probability. If the
6
6
  * criteria leave the middle open, an ambiguous condition lands somewhere in the
7
7
  * middle and the two-sided thresholds in `decide.ts` can route it to a human.
package/src/jev/decide.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * p <= 1 - t → rejected ("the opposite is as certain as a pass would be")
8
8
  * in between → the middle band
9
9
  *
10
- * The middle band is not a bug to be squeezed out. Measured JEV answers sit at
10
+ * The middle band is not a bug to be squeezed out. Measured Jev answers sit at
11
11
  * 0.98/0.02 for clear cases but also at 0.85–0.95 for conditions that are clear
12
12
  * to a human and merely not certain to the model, so a single high bar would
13
13
  * report almost everything as uncertain. What the middle band *means* depends on
package/src/jev/engine.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The JEV decision engine.
2
+ * The Jev decision engine.
3
3
  *
4
4
  * One tool call in, one request out. All conditions for the call travel in the
5
- * same request because JEV answers them in parallel and independently, so the
5
+ * same request because Jev answers them in parallel and independently, so the
6
6
  * marginal cost of an extra condition is a few tokens rather than a round trip.
7
7
  *
8
8
  * Everything that can go wrong resolves to `unavailable`, and the caller turns
@@ -43,12 +43,12 @@ export interface JevEngineOptions {
43
43
  export const DEFAULT_MAX_STATE_CHARACTERS = 120_000;
44
44
 
45
45
  const UNAVAILABLE_TEXT: Record<string, string> = {
46
- timeout: "the JEV request timed out",
47
- network: "the JEV request could not reach the API",
48
- http: "the JEV API returned an error status",
49
- malformed_response: "the JEV response did not match the questions that were asked",
46
+ timeout: "the Jev request timed out",
47
+ network: "the Jev request could not reach the API",
48
+ http: "the Jev API returned an error status",
49
+ malformed_response: "the Jev response did not match the questions that were asked",
50
50
  state_too_large: "the call description exceeded the request budget",
51
- unknown: "the JEV request failed for an unknown reason",
51
+ unknown: "the Jev request failed for an unknown reason",
52
52
  };
53
53
 
54
54
  export function createJevEngine(options: JevEngineOptions): DecisionEngine {
@@ -99,7 +99,7 @@ export function createJevEngine(options: JevEngineOptions): DecisionEngine {
99
99
  return {
100
100
  verdict: "unavailable",
101
101
  reason: result.reason,
102
- rationale: UNAVAILABLE_TEXT[result.reason] ?? UNAVAILABLE_TEXT.unknown ?? "JEV could not decide.",
102
+ rationale: UNAVAILABLE_TEXT[result.reason] ?? UNAVAILABLE_TEXT.unknown ?? "Jev could not decide.",
103
103
  latencyMs,
104
104
  };
105
105
  }
@@ -109,7 +109,7 @@ export function createJevEngine(options: JevEngineOptions): DecisionEngine {
109
109
  return {
110
110
  verdict: "unavailable",
111
111
  reason: parsed.reason,
112
- rationale: UNAVAILABLE_TEXT[parsed.reason] ?? "The JEV response could not be used.",
112
+ rationale: UNAVAILABLE_TEXT[parsed.reason] ?? "The Jev response could not be used.",
113
113
  latencyMs,
114
114
  };
115
115
  }
package/src/jev/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JEV layer: transport, question set, response validation, and the engine.
2
+ * Jev layer: transport, question set, response validation, and the engine.
3
3
  */
4
4
 
5
5
  export { createJevEngine, DEFAULT_MAX_STATE_CHARACTERS, type JevEngineOptions, type ObservationMeta } from "./engine.ts";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The question set.
3
3
  *
4
- * Two things learned from real JEV answers (see `docs/calibration.md`) shaped
4
+ * Two things learned from real Jev answers (see `docs/calibration.md`) shaped
5
5
  * this design:
6
6
  *
7
7
  * 1. **"Absence of a hazard" questions cluster between 0.75 and 0.98.** Asking
@@ -15,7 +15,7 @@
15
15
  * for". That is the `required` condition. Everything else answers "is a
16
16
  * specific bad thing happening", and a clear "yes, it is" should block.
17
17
  *
18
- * JEV evaluates questions in parallel and independently and does not send the
18
+ * Jev evaluates questions in parallel and independently and does not send the
19
19
  * question keys to the model, so each instruction must stand alone.
20
20
  */
21
21
 
package/src/jev/state.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Shapes shared with the JEV layer.
2
+ * Shapes shared with the Jev layer.
3
3
  *
4
4
  * These are types only. The transport, question set, and decision mapping live in
5
5
  * this directory too, but they arrive in a later milestone.
6
6
  */
7
7
 
8
- /** A JSON value accepted by the JEV `state` field. `Date` / `Map` are not included. */
8
+ /** A JSON value accepted by the Jev `state` field. `Date` / `Map` are not included. */
9
9
  export type JevJson = string | number | boolean | null | JevJson[] | { [key: string]: JevJson };
10
10
 
11
11
  /**
package/src/jev/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The JEV transport contract.
2
+ * The Jev transport contract.
3
3
  *
4
4
  * The interface is deliberately narrow and its result type is normalized: the
5
5
  * official SDK's exception hierarchy stops at `transport.ts`, so the engine and
package/src/policy.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Deterministic policy layer.
3
3
  *
4
- * Everything in this module runs before JEV. Hard-deny rules are deliberate,
4
+ * Everything in this module runs before Jev. Hard-deny rules are deliberate,
5
5
  * non-overridable, and must never be reachable by a probabilistic decision: they
6
6
  * are the floor that keeps a mis-calibrated semantic verdict from becoming an
7
7
  * approved `rm -rf /`.
@@ -13,9 +13,9 @@
13
13
  import { isAbsolute, relative, resolve, sep } from "node:path";
14
14
 
15
15
  export interface CommandRuleConfig {
16
- /** Shell-style `*` / `?` patterns that auto-approve without asking JEV. */
16
+ /** Shell-style `*` / `?` patterns that auto-approve without asking Jev. */
17
17
  readonly allowedCommands: readonly string[];
18
- /** Shell-style patterns that block immediately, before JEV. */
18
+ /** Shell-style patterns that block immediately, before Jev. */
19
19
  readonly disallowedCommands: readonly string[];
20
20
  }
21
21
 
@@ -196,7 +196,7 @@ const DANGEROUS_PATTERNS: readonly CommandPattern[] = [
196
196
  ];
197
197
 
198
198
  /**
199
- * Catastrophic targets. These are never handed to JEV: a look-alike approval
199
+ * Catastrophic targets. These are never handed to Jev: a look-alike approval
200
200
  * would be unsafe even when the surrounding conversation seems to ask for it.
201
201
  *
202
202
  * The list is deliberately small. Everything else belongs to the semantic layer,
@@ -437,7 +437,7 @@ export interface WriteTarget {
437
437
  * Classify a write/edit target lexically.
438
438
  *
439
439
  * A symlink inside the working directory can still point outside it; resolving
440
- * that needs a filesystem call and belongs to the JEV layer's state building.
440
+ * that needs a filesystem call and belongs to the Jev layer's state building.
441
441
  */
442
442
  export function classifyWriteTarget(
443
443
  inputPath: string,
package/src/settings.ts CHANGED
@@ -16,7 +16,7 @@ import { dirname, join } from "node:path";
16
16
 
17
17
  export interface JevAutoModeSettings {
18
18
  readonly enabled: boolean;
19
- /** Per-attempt JEV timeout. Kept short: this is a gate, not a batch job. */
19
+ /** Per-attempt Jev timeout. Kept short: this is a gate, not a batch job. */
20
20
  readonly timeoutMs: number;
21
21
  /** Retries after the first attempt. */
22
22
  readonly maxRetries: number;
@@ -28,6 +28,8 @@ export interface JevAutoModeSettings {
28
28
  readonly extraProtectedPaths: readonly string[];
29
29
  /** Shared state + questions budget guard, in characters. */
30
30
  readonly maxStateCharacters: number;
31
+ /** What a middle-band judgment means. Default `deny`: no user confirmation. */
32
+ readonly uncertain: UncertainAction;
31
33
  /**
32
34
  * Per-rule probability thresholds, overriding the calibrated defaults.
33
35
  *
@@ -39,6 +41,18 @@ export interface JevAutoModeSettings {
39
41
 
40
42
  export type SettingsScope = "global" | "project";
41
43
 
44
+ /**
45
+ * How a judgment that lands in the middle band is resolved.
46
+ *
47
+ * `deny` (the default) means the gate never takes over the screen: Jev's probability
48
+ * is the whole answer, and "not sure" fails closed like every other undecidable
49
+ * state. `ask` hands the call to the user, which contradicts the point of an auto
50
+ * mode and is therefore opt-in. `allow` trusts the middle band.
51
+ */
52
+ export type UncertainAction = "deny" | "ask" | "allow";
53
+
54
+ export const UNCERTAIN_ACTIONS: readonly UncertainAction[] = ["deny", "ask", "allow"];
55
+
42
56
  export const DEFAULT_SETTINGS: JevAutoModeSettings = {
43
57
  enabled: true,
44
58
  timeoutMs: 4000,
@@ -48,6 +62,7 @@ export const DEFAULT_SETTINGS: JevAutoModeSettings = {
48
62
  disallowedCommands: [],
49
63
  extraProtectedPaths: [],
50
64
  maxStateCharacters: 120_000,
65
+ uncertain: "deny",
51
66
  thresholds: {},
52
67
  };
53
68
 
@@ -101,6 +116,10 @@ function readThresholds(value: unknown): Readonly<Record<string, number>> | unde
101
116
  return thresholds;
102
117
  }
103
118
 
119
+ export function isUncertainAction(value: unknown): value is UncertainAction {
120
+ return typeof value === "string" && UNCERTAIN_ACTIONS.includes(value as UncertainAction);
121
+ }
122
+
104
123
  function readBoundedInteger(value: unknown, min: number, max: number): number | undefined {
105
124
  if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
106
125
  const rounded = Math.round(value);
@@ -140,6 +159,10 @@ export function parseSettingsPatch(value: unknown): SettingsPatch {
140
159
  const maxStateCharacters = readBoundedInteger(record.maxStateCharacters, 1000, 1_000_000);
141
160
  if (maxStateCharacters !== undefined) patch.maxStateCharacters = maxStateCharacters;
142
161
 
162
+ if (record.uncertain !== undefined && isUncertainAction(record.uncertain)) {
163
+ patch.uncertain = record.uncertain;
164
+ }
165
+
143
166
  const safeCommands = record.safeCommands === undefined ? undefined : readStringArray(record.safeCommands);
144
167
  if (safeCommands !== undefined) patch.safeCommands = safeCommands;
145
168
 
@@ -226,7 +249,7 @@ export class JevAutoModeStore {
226
249
  await writeFileAtomic(path, `${JSON.stringify(settings, null, 2)}\n`);
227
250
  }
228
251
 
229
- /** User-authored policy notes. Advisory input to JEV, never a hard rule. */
252
+ /** User-authored policy notes. Advisory input to Jev, never a hard rule. */
230
253
  async loadPolicyNotes(): Promise<string> {
231
254
  try {
232
255
  return (await readFile(this.policyNotesPath(), "utf8")).slice(0, MAX_POLICY_NOTES_LENGTH);
package/src/ui.ts CHANGED
@@ -43,9 +43,97 @@ export function describeSettings(settings: JevAutoModeSettings, scope: SettingsS
43
43
  `disallowed commands: ${settings.disallowedCommands.length}`,
44
44
  `extra protected paths: ${settings.extraProtectedPaths.length}`,
45
45
  `max state characters: ${settings.maxStateCharacters}`,
46
+ `uncertain band: ${settings.uncertain}`,
46
47
  ].join("\n");
47
48
  }
48
49
 
50
+ /**
51
+ * A bounded rendering of a command, for dialogs.
52
+ *
53
+ * Pi's dialogs do not clip their content: a 60-line title fills the pane and pushes
54
+ * the dialog's own heading off screen, and opening and closing one per tool call
55
+ * makes the terminal scroll back and forth. So the preview is bounded here, and the
56
+ * full command stays where it already is — in the tool call above the dialog.
57
+ */
58
+ export interface CommandPreview {
59
+ readonly lines: readonly string[];
60
+ readonly truncated: boolean;
61
+ readonly hiddenLines: number;
62
+ readonly hiddenCharacters: number;
63
+ }
64
+
65
+ export const DEFAULT_PREVIEW_LINES = 6;
66
+ export const DEFAULT_PREVIEW_LINE_LENGTH = 120;
67
+
68
+ export function previewCommand(
69
+ command: string,
70
+ options: { readonly maxLines?: number; readonly maxLineLength?: number } = {},
71
+ ): CommandPreview {
72
+ const maxLines = options.maxLines ?? DEFAULT_PREVIEW_LINES;
73
+ const maxLineLength = options.maxLineLength ?? DEFAULT_PREVIEW_LINE_LENGTH;
74
+
75
+ const all = command.split("\n");
76
+ const kept = all.slice(0, maxLines);
77
+ const lines = kept.map((line) => (line.length > maxLineLength ? `${line.slice(0, maxLineLength)}…` : line));
78
+ const hidden = all.slice(maxLines);
79
+
80
+ // A single 4000-character line has no hidden lines, but most of it was still cut.
81
+ const cutCharacters = kept.reduce((total, line, index) => total + Math.max(0, line.length - (lines[index]?.length ?? 0)), 0);
82
+ const hiddenCharacters = hidden.reduce((total, line) => total + line.length + 1, 0) + cutCharacters;
83
+
84
+ return {
85
+ lines,
86
+ truncated: hidden.length > 0 || cutCharacters > 0,
87
+ hiddenLines: hidden.length,
88
+ hiddenCharacters,
89
+ };
90
+ }
91
+
92
+ /** Cut a text block to a line budget, marking what was dropped. */
93
+ export function clampLines(text: string, maxLines: number): string {
94
+ const lines = text.split("\n");
95
+ if (lines.length <= maxLines) return text;
96
+ return [...lines.slice(0, maxLines - 1), `… (${lines.length - maxLines + 1} more lines)`].join("\n");
97
+ }
98
+
99
+ export interface ConfirmationParts {
100
+ readonly tool: string;
101
+ readonly command?: string;
102
+ readonly path?: string;
103
+ readonly reasons: readonly string[];
104
+ readonly rationale: string;
105
+ }
106
+
107
+ /** The dialog shown when a judgment is delegated to the user. */
108
+ export const CONFIRMATION_MAX_LINES = 14;
109
+
110
+ export function buildConfirmationDialog(parts: ConfirmationParts): string {
111
+ const preview = parts.command === undefined ? undefined : previewCommand(parts.command);
112
+ const hiddenNote =
113
+ preview?.truncated === true
114
+ ? `… ${[
115
+ preview.hiddenLines > 0 ? `${preview.hiddenLines} more line(s)` : undefined,
116
+ `${preview.hiddenCharacters} more character(s)`,
117
+ ]
118
+ .filter(Boolean)
119
+ .join(", ")} — the full command is in the tool call above`
120
+ : undefined;
121
+
122
+ return clampLines(
123
+ [
124
+ "Jev auto mode wants confirmation before this runs.",
125
+ `Tool: ${parts.tool}`,
126
+ ...(preview?.lines ?? []),
127
+ ...(hiddenNote === undefined ? [] : [hiddenNote]),
128
+ ...(parts.path === undefined ? [] : [parts.path]),
129
+ "",
130
+ `Matched: ${parts.reasons.join(", ")}`,
131
+ parts.rationale,
132
+ ].join("\n"),
133
+ CONFIRMATION_MAX_LINES,
134
+ );
135
+ }
136
+
49
137
  export const USAGE_TEXT = [
50
138
  "Usage:",
51
139
  " /jev-auto-mode show status",
@@ -57,10 +145,20 @@ export const USAGE_TEXT = [
57
145
  " /jev-auto-mode threshold show thresholds and last observed probabilities",
58
146
  " /jev-auto-mode threshold <rule> <0.5-1.0>",
59
147
  " /jev-auto-mode threshold reset [rule]",
148
+ " /jev-auto-mode threshold edit pick a rule and type a value",
149
+ " /jev-auto-mode uncertain show what the middle band resolves to",
150
+ " /jev-auto-mode uncertain deny|ask|allow",
151
+ ].join("\n");
152
+
153
+ export const UNCERTAIN_EXPLANATION = [
154
+ "The middle band is where Jev is neither satisfied nor rejecting.",
155
+ " deny - block it. No prompt, no screen takeover: Jev's probability is the answer.",
156
+ " ask - hand the call to the user. Opt-in, because it hands the decision back to a human.",
157
+ " allow - let it through. Trusts the band; the least safe of the three.",
60
158
  ].join("\n");
61
159
 
62
160
  export const POLICY_HEADER = [
63
- "# JEV auto mode policy",
161
+ "# Jev auto mode policy",
64
162
  "",
65
163
  "Free-form notes describing what this machine and these repositories allow.",
66
164
  "They are reference material for the semantic judgment: they can justify an",