pi-jev-auto-mode 0.4.0 → 0.5.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 +26 -0
- package/README.md +13 -5
- package/package.json +1 -1
- package/src/decide.ts +2 -3
- package/src/extension.ts +52 -8
- package/src/jev/availability.ts +6 -4
- package/src/policy.ts +19 -0
- package/src/records.ts +68 -2
- package/src/settings.ts +29 -6
- package/src/ui.ts +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-09-24
|
|
4
|
+
|
|
5
|
+
- **Decision records are compact by default.** An approval is drawn as one line — status, tool,
|
|
6
|
+
latency, and the command — and a block as two, keeping the rationale. Each record used to take
|
|
7
|
+
five padded blocks, so a long session was mostly gate records. `/jev-auto-mode display full`
|
|
8
|
+
(or `"display": "full"`) restores the previous view, and the expanded view shows every detail
|
|
9
|
+
in either mode.
|
|
10
|
+
|
|
11
|
+
## 0.4.1 - 2026-09-18
|
|
12
|
+
|
|
13
|
+
- **An unclear answer passes by default.** `uncertain` defaulted to `deny`, which meant a
|
|
14
|
+
judgment the model was unsure about stopped the call. That is the interruption an auto mode
|
|
15
|
+
exists to remove; clear rejections still block. `uncertain deny` and `uncertain ask` remain
|
|
16
|
+
available for anyone who wants the stricter behaviour.
|
|
17
|
+
- **Without a key the gate says so and stops**, instead of inventing a verdict and blocking with
|
|
18
|
+
an unexplained reason. The message names the fix: `/jev-auto-mode login`, or
|
|
19
|
+
`/jev-auto-mode off`. The footer reads `🛡 jev no key` in that state, and the session start
|
|
20
|
+
warns once.
|
|
21
|
+
- **A chain of read-only commands is read-only.** `cd src && ls -la && git log -3` was judged as
|
|
22
|
+
a whole because the allowlist rejected any command containing shell control syntax, so agents
|
|
23
|
+
paid a judgment round trip for their most common line. Each segment is now checked on its own,
|
|
24
|
+
and `cd` is allowed. A chain containing anything else (`curl … | sh`) is still judged.
|
|
25
|
+
- **The docs match the no-key behaviour.** The README and a comment still described a fallback
|
|
26
|
+
that confirms in a UI, which the gate no longer does: without a key it stops the calls it cannot
|
|
27
|
+
judge and says it is not connected to Jev.
|
|
28
|
+
|
|
3
29
|
|
|
4
30
|
## 0.4.0
|
|
5
31
|
|
package/README.md
CHANGED
|
@@ -113,15 +113,22 @@ Packages are discovered in the [package gallery](https://pi.dev/packages) throug
|
|
|
113
113
|
/jev-auto-mode uncertain show what the middle band resolves to
|
|
114
114
|
/jev-auto-mode uncertain deny|ask|allow
|
|
115
115
|
/jev-auto-mode scope all|matched how far the semantic layer reaches
|
|
116
|
+
/jev-auto-mode display full|compact how decision records are drawn
|
|
116
117
|
```
|
|
117
118
|
|
|
119
|
+
Each decision leaves a record in the transcript. `display: "compact"` (the default) folds an
|
|
120
|
+
approval into one line and a block into two (the rationale stays, because a block is something
|
|
121
|
+
to act on). `display: "full"` shows the heading, the engine line, the command, the reasons, and
|
|
122
|
+
the rationale. The expanded view shows every detail in either mode.
|
|
123
|
+
|
|
118
124
|
```
|
|
119
125
|
pi --jev-auto-mode start with auto mode enabled
|
|
120
126
|
```
|
|
121
127
|
|
|
122
128
|
The semantic layer needs a [TypeSafe](https://typesafe.ai/) API key. Jev is early access, so an
|
|
123
|
-
account may be waitlisted; **the gate still works without one
|
|
124
|
-
|
|
129
|
+
account may be waitlisted; **the gate still works without one**. Its own rules keep running —
|
|
130
|
+
read-only and user-declared safe commands pass, hard-deny shapes are blocked — but a call
|
|
131
|
+
nothing vouches for is blocked with "Not connected to Jev" instead of being judged.
|
|
125
132
|
|
|
126
133
|
`/jev-auto-mode login` asks for the key, verifies it against the API (`GET /v1/models`), and
|
|
127
134
|
stores it as an owner-only file at
|
|
@@ -134,9 +141,9 @@ A key is only stored after the API accepts it: a typo that got saved would turn
|
|
|
134
141
|
that silently blocks every escalated call. If the API cannot be reached the key is not stored
|
|
135
142
|
either, and the command says so rather than claiming success.
|
|
136
143
|
|
|
137
|
-
Without a key the gate does not disable itself
|
|
138
|
-
|
|
139
|
-
semantic layer is active and `🛡 jev
|
|
144
|
+
Without a key the gate does not disable itself and does not allow everything: it stops the calls
|
|
145
|
+
it cannot judge and says it is not connected to Jev. The footer shows `🛡 jev (<scope>)` while the
|
|
146
|
+
semantic layer is active and `🛡 jev no key (<scope>)` when it is not.
|
|
140
147
|
|
|
141
148
|
## Tuning
|
|
142
149
|
|
|
@@ -194,6 +201,7 @@ Policy notes: `$PI_CODING_AGENT_DIR/jev-auto-mode-policy.md`.
|
|
|
194
201
|
"maxStateCharacters": 120000,
|
|
195
202
|
"uncertain": "deny",
|
|
196
203
|
"gateScope": "all",
|
|
204
|
+
"display": "compact",
|
|
197
205
|
"thresholds": {}
|
|
198
206
|
}
|
|
199
207
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-jev-auto-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
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",
|
package/src/decide.ts
CHANGED
|
@@ -75,9 +75,8 @@ export interface DecisionEngine {
|
|
|
75
75
|
/**
|
|
76
76
|
* Milestone-1 engine: no semantic judgment at all.
|
|
77
77
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* verifiable on its own, and it fails in the safe direction.
|
|
78
|
+
* The gate checks for this engine before judging: with no key there is nothing to
|
|
79
|
+
* judge with, so it says so and blocks the call instead of inventing a verdict.
|
|
81
80
|
*/
|
|
82
81
|
export function createManualEngine(): DecisionEngine {
|
|
83
82
|
return {
|
package/src/extension.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
dangerousReasons,
|
|
42
42
|
evaluateUserCommandRules,
|
|
43
43
|
hardDenyReasons,
|
|
44
|
-
|
|
44
|
+
isReadOnlyCommandChain,
|
|
45
45
|
isUserDeclaredSafeCommand,
|
|
46
46
|
PROTECTED_DIRECTORY_SEGMENTS,
|
|
47
47
|
unique,
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
import {
|
|
56
56
|
DEFAULT_SETTINGS,
|
|
57
57
|
JevAutoModeStore,
|
|
58
|
+
isDisplayMode,
|
|
58
59
|
isGateScope,
|
|
59
60
|
isUncertainAction,
|
|
60
61
|
parseThreshold,
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
import {
|
|
65
66
|
buildConfirmationDialog,
|
|
66
67
|
describeSettings,
|
|
68
|
+
DISPLAY_EXPLANATION,
|
|
67
69
|
GATE_SCOPE_EXPLANATION,
|
|
68
70
|
UNCERTAIN_EXPLANATION,
|
|
69
71
|
formatRuleTable,
|
|
@@ -80,6 +82,13 @@ export const AUTO_MODE_COMMAND = "jev-auto-mode";
|
|
|
80
82
|
/** The escalation reason for a call no pattern describes, under `gateScope: "all"`. */
|
|
81
83
|
export const NOT_KNOWN_SAFE_REASON = "not on the known-safe list";
|
|
82
84
|
|
|
85
|
+
/** The engine used when no semantic layer is available. */
|
|
86
|
+
export const MANUAL_ENGINE_ID = "manual";
|
|
87
|
+
|
|
88
|
+
/** Shown (and used as the block reason) when the gate has no Jev connection. */
|
|
89
|
+
export const NO_ENGINE_MESSAGE =
|
|
90
|
+
"Not connected to Jev (no TypeSafe API key is set). Run `/jev-auto-mode login` to set a key, or `/jev-auto-mode off` to stop auto mode.";
|
|
91
|
+
|
|
83
92
|
/** Structural context: what this extension needs from Pi, and nothing more. */
|
|
84
93
|
export interface GateUi {
|
|
85
94
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
@@ -248,7 +257,7 @@ export async function evaluateToolCall(
|
|
|
248
257
|
const matchedReasons = dangerousReasons(command, ctx.cwd);
|
|
249
258
|
if (matchedReasons.length > 0) {
|
|
250
259
|
reasons = matchedReasons;
|
|
251
|
-
} else if (
|
|
260
|
+
} else if (isReadOnlyCommandChain(command)) {
|
|
252
261
|
return undefined;
|
|
253
262
|
} else if (state.settings.gateScope === "matched") {
|
|
254
263
|
return undefined;
|
|
@@ -265,6 +274,20 @@ export async function evaluateToolCall(
|
|
|
265
274
|
reasons = protectedReasons;
|
|
266
275
|
}
|
|
267
276
|
|
|
277
|
+
// Without a key there is nothing to judge with. Say so and stop, rather than
|
|
278
|
+
// letting a call through unjudged or blocking it with an unexplained verdict.
|
|
279
|
+
if (deps.engine.id === MANUAL_ENGINE_ID) {
|
|
280
|
+
const rationale = NO_ENGINE_MESSAGE;
|
|
281
|
+
writeRecord(deps, {
|
|
282
|
+
call,
|
|
283
|
+
reasons,
|
|
284
|
+
status: "blocked",
|
|
285
|
+
source: "unavailable",
|
|
286
|
+
rationale,
|
|
287
|
+
});
|
|
288
|
+
return { block: true, reason: rationale };
|
|
289
|
+
}
|
|
290
|
+
|
|
268
291
|
const input: CandidateInput = {
|
|
269
292
|
call,
|
|
270
293
|
reasons,
|
|
@@ -460,8 +483,9 @@ export interface RegisterOptions {
|
|
|
460
483
|
/**
|
|
461
484
|
* Build the semantic engine for the current settings.
|
|
462
485
|
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
486
|
+
* With no key the gate still runs its own rules, but a call nothing vouches for is
|
|
487
|
+
* blocked with "Not connected to Jev" rather than judged. A missing key must not
|
|
488
|
+
* turn into "allow everything".
|
|
465
489
|
*/
|
|
466
490
|
export function createEngine(
|
|
467
491
|
settings: JevAutoModeSettings,
|
|
@@ -534,6 +558,9 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
|
|
|
534
558
|
await rebuildEngine();
|
|
535
559
|
loaded = true;
|
|
536
560
|
updateStatus(ctx, { enabled: state.settings.enabled, engineId: deps.engine.id, scope: state.scope });
|
|
561
|
+
if (state.settings.enabled && deps.engine.id === MANUAL_ENGINE_ID) {
|
|
562
|
+
ctx.ui.notify(NO_ENGINE_MESSAGE, "warning");
|
|
563
|
+
}
|
|
537
564
|
};
|
|
538
565
|
|
|
539
566
|
const save = async (ctx: GateContext): Promise<void> => {
|
|
@@ -546,7 +573,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
|
|
|
546
573
|
default: false,
|
|
547
574
|
});
|
|
548
575
|
|
|
549
|
-
registerDecisionEntryRenderer(pi);
|
|
576
|
+
registerDecisionEntryRenderer(pi, () => state.settings.display);
|
|
550
577
|
|
|
551
578
|
pi.registerCommand(AUTO_MODE_COMMAND, {
|
|
552
579
|
description: "Show or change the Jev auto mode settings",
|
|
@@ -554,7 +581,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
|
|
|
554
581
|
const value = String(argumentPrefix ?? "");
|
|
555
582
|
const tokens = value.split(/\s+/).filter(Boolean);
|
|
556
583
|
if (tokens.length === 0) {
|
|
557
|
-
return ["status", "on", "off", "policy", "threshold", "scope", "uncertain", "login", "logout"].map((item) => ({
|
|
584
|
+
return ["status", "on", "off", "policy", "threshold", "scope", "uncertain", "display", "login", "logout"].map((item) => ({
|
|
558
585
|
value: item,
|
|
559
586
|
label: item,
|
|
560
587
|
}));
|
|
@@ -685,7 +712,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
|
|
|
685
712
|
"Remove the stored TypeSafe API key?",
|
|
686
713
|
availability.source === "env"
|
|
687
714
|
? "It is not in use anyway: TYPESAFE_API_KEY takes precedence."
|
|
688
|
-
: "
|
|
715
|
+
: "Without a key the gate blocks every call it cannot vouch for, and says why.",
|
|
689
716
|
);
|
|
690
717
|
if (!confirmed) return;
|
|
691
718
|
|
|
@@ -699,7 +726,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
|
|
|
699
726
|
ctx.ui.notify(
|
|
700
727
|
availability.available
|
|
701
728
|
? `Stored key removed. Still using ${describeKeySource(availability.source)}.`
|
|
702
|
-
: "Stored key removed. The
|
|
729
|
+
: "Stored key removed. The gate will block calls it cannot vouch for until a key is set again.",
|
|
703
730
|
"info",
|
|
704
731
|
);
|
|
705
732
|
return;
|
|
@@ -731,6 +758,23 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
|
|
|
731
758
|
return;
|
|
732
759
|
}
|
|
733
760
|
|
|
761
|
+
if (value.startsWith("display")) {
|
|
762
|
+
const argument = value.slice("display".length).trim();
|
|
763
|
+
if (argument === "") {
|
|
764
|
+
ctx.ui.notify(`display: ${state.settings.display}\n\n${DISPLAY_EXPLANATION}`, "info");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (!isDisplayMode(argument)) {
|
|
768
|
+
ctx.ui.notify(`Expected one of full, compact.\n\n${DISPLAY_EXPLANATION}`, "error");
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
state.settings = { ...state.settings, display: argument };
|
|
773
|
+
await save(gateContext);
|
|
774
|
+
ctx.ui.notify(`decision records now display as: ${argument}`, "info");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
734
778
|
if (value.startsWith("uncertain")) {
|
|
735
779
|
const argument = value.slice("uncertain".length).trim();
|
|
736
780
|
if (argument === "") {
|
package/src/jev/availability.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Is the semantic layer usable right now, and where did the key come from?
|
|
3
3
|
*
|
|
4
|
-
* A missing key must not silently degrade into "allow everything"
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A missing key must not silently degrade into "allow everything". The gate keeps
|
|
5
|
+
* running its own rules — read-only and user-declared safe commands pass, hard-deny
|
|
6
|
+
* patterns are blocked — but a call nothing vouches for is blocked with "Not
|
|
7
|
+
* connected to Jev" instead of being judged. The reason and the key's origin are
|
|
8
|
+
* surfaced in `/jev-auto-mode status` so the situation is visible rather than
|
|
9
|
+
* mysterious.
|
|
8
10
|
*
|
|
9
11
|
* `TYPESAFE_API_KEY` wins over the stored secret, so a one-off or CI override does
|
|
10
12
|
* not require touching the stored credential.
|
package/src/policy.ts
CHANGED
|
@@ -45,6 +45,7 @@ export interface CommandPattern {
|
|
|
45
45
|
export const SAFE_COMMANDS: readonly string[] = [
|
|
46
46
|
// Shell state and navigation
|
|
47
47
|
"pwd",
|
|
48
|
+
"cd*",
|
|
48
49
|
"ls*",
|
|
49
50
|
"tree*",
|
|
50
51
|
"whoami",
|
|
@@ -392,6 +393,24 @@ export function isReadOnlyCommand(command: string): boolean {
|
|
|
392
393
|
return matchesAnyCommandPattern(command, SAFE_COMMANDS, false) !== undefined;
|
|
393
394
|
}
|
|
394
395
|
|
|
396
|
+
/**
|
|
397
|
+
* A chain of read-only commands, such as `cd src && ls -la && git log`.
|
|
398
|
+
*
|
|
399
|
+
* Agents chain commands constantly, and a single `&&` would otherwise take an
|
|
400
|
+
* otherwise harmless line out of the fast path and into a judgment round trip.
|
|
401
|
+
* Every segment must be read-only on its own; `curl … | sh` splits into `curl …`
|
|
402
|
+
* (not on the list) and `sh` (not on the list), so it is still judged, and a
|
|
403
|
+
* segment containing a redirection fails the matcher anyway.
|
|
404
|
+
*/
|
|
405
|
+
export function isReadOnlyCommandChain(command: string): boolean {
|
|
406
|
+
const segments = command
|
|
407
|
+
.split(/&&|\|\||;|\||\n/)
|
|
408
|
+
.map((segment) => segment.trim())
|
|
409
|
+
.filter((segment) => segment.length > 0);
|
|
410
|
+
if (segments.length === 0) return false;
|
|
411
|
+
return segments.every((segment) => isReadOnlyCommand(segment));
|
|
412
|
+
}
|
|
413
|
+
|
|
395
414
|
/**
|
|
396
415
|
* Patterns the user declared safe.
|
|
397
416
|
*
|
package/src/records.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* recorded rationale should not become ammunition for the next tool call.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { Box, Text } from "@earendil-works/pi-tui";
|
|
9
|
+
import { Box, Text, truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import type { ConditionReport, DecisionSource } from "./decide.ts";
|
|
12
12
|
import { formatThreshold } from "./jev/decide.ts";
|
|
13
|
+
import type { DisplayMode } from "./settings.ts";
|
|
13
14
|
|
|
14
15
|
export const DECISION_ENTRY_TYPE = "jev-auto-mode-decision";
|
|
15
16
|
|
|
@@ -85,12 +86,77 @@ export function formatConditionLines(record: DecisionRecord): string[] {
|
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
/** Collapse whitespace so a multi-line command reads as one line. */
|
|
90
|
+
function oneLine(text: string): string {
|
|
91
|
+
return text.replace(/\s+/g, " ").trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface CompactDecisionLines {
|
|
95
|
+
/** `🛡 jev allowed · bash · 299ms · <command>`: everything an approval needs. */
|
|
96
|
+
readonly headline: string;
|
|
97
|
+
/**
|
|
98
|
+
* The rationale, only for a call that did not run. An approval does not need its
|
|
99
|
+
* reasoning on screen; a block does, because the user has to act on it.
|
|
100
|
+
*/
|
|
101
|
+
readonly detail?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The compact view: one line for an approval, two for a block.
|
|
106
|
+
*
|
|
107
|
+
* Width is applied at render time. This only decides what the lines say.
|
|
108
|
+
*/
|
|
109
|
+
export function formatCompactLines(record: DecisionRecord): CompactDecisionLines {
|
|
110
|
+
const approved = record.status === "allowed" || record.status === "confirmed";
|
|
111
|
+
const parts = [`${approved ? "🛡" : "⛔"} jev ${STATUS_LABEL[record.status]}`, record.tool];
|
|
112
|
+
if (!approved) parts.push(`via ${record.source}`);
|
|
113
|
+
if (typeof record.latencyMs === "number") parts.push(`${Math.round(record.latencyMs)}ms`);
|
|
114
|
+
const summary = oneLine(record.summary);
|
|
115
|
+
if (summary.length > 0) parts.push(summary);
|
|
116
|
+
const headline = parts.join(" · ");
|
|
117
|
+
if (approved) return { headline };
|
|
118
|
+
return { headline, detail: oneLine(record.rationale) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A single line clipped to the viewport instead of wrapped. */
|
|
122
|
+
class ClippedLine implements Component {
|
|
123
|
+
private readonly text: string;
|
|
124
|
+
|
|
125
|
+
constructor(text: string) {
|
|
126
|
+
this.text = text;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
render(width: number): string[] {
|
|
130
|
+
return [truncateToWidth(this.text, Math.max(1, width))];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
invalidate(): void {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type DisplayModeSource = () => DisplayMode;
|
|
137
|
+
|
|
138
|
+
export function registerDecisionEntryRenderer(
|
|
139
|
+
pi: Pick<ExtensionAPI, "registerEntryRenderer">,
|
|
140
|
+
displayMode: DisplayModeSource = () => "compact",
|
|
141
|
+
): void {
|
|
89
142
|
pi.registerEntryRenderer<DecisionRecord>(DECISION_ENTRY_TYPE, (entry, options, theme) => {
|
|
90
143
|
const record = entry.data;
|
|
91
144
|
if (!record) return undefined;
|
|
92
145
|
|
|
93
146
|
const approved = record.status === "allowed" || record.status === "confirmed";
|
|
147
|
+
|
|
148
|
+
// The expanded view is the place to read a decision in full, so it ignores the
|
|
149
|
+
// compact setting.
|
|
150
|
+
if (displayMode() === "compact" && !options.expanded) {
|
|
151
|
+
const lines = formatCompactLines(record);
|
|
152
|
+
const box = new Box(1, 0, (text) => theme.bg("customMessageBg", text));
|
|
153
|
+
box.addChild(new ClippedLine(theme.fg(approved ? "muted" : "error", lines.headline)));
|
|
154
|
+
if (lines.detail !== undefined) {
|
|
155
|
+
box.addChild(new ClippedLine(theme.fg("dim", `rationale: ${lines.detail}`)));
|
|
156
|
+
}
|
|
157
|
+
return box;
|
|
158
|
+
}
|
|
159
|
+
|
|
94
160
|
const icon = approved ? "🛡" : "⛔";
|
|
95
161
|
const heading = `${icon} ${theme.bold("jev auto mode")} ${theme.fg(
|
|
96
162
|
approved ? "success" : "error",
|
package/src/settings.ts
CHANGED
|
@@ -28,7 +28,7 @@ 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 `
|
|
31
|
+
/** What a middle-band judgment means. Default `allow`: no user confirmation. */
|
|
32
32
|
readonly uncertain: UncertainAction;
|
|
33
33
|
/**
|
|
34
34
|
* Which calls reach the semantic layer.
|
|
@@ -45,6 +45,23 @@ export interface JevAutoModeSettings {
|
|
|
45
45
|
* visible in `/jev-auto-mode threshold` instead of silently resetting the rule.
|
|
46
46
|
*/
|
|
47
47
|
readonly thresholds: Readonly<Record<string, number>>;
|
|
48
|
+
/**
|
|
49
|
+
* How decision records are drawn in the transcript.
|
|
50
|
+
*
|
|
51
|
+
* `compact` (the default) folds an approval into one line and a block into two, so
|
|
52
|
+
* a long session is not dominated by gate records. `full` shows the heading, the
|
|
53
|
+
* engine line, the command, the reasons, and the rationale. The expanded view (the tool
|
|
54
|
+
* expansion key) always shows every detail, whichever mode is set.
|
|
55
|
+
*/
|
|
56
|
+
readonly display: DisplayMode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type DisplayMode = "full" | "compact";
|
|
60
|
+
|
|
61
|
+
export const DISPLAY_MODES: readonly DisplayMode[] = ["full", "compact"];
|
|
62
|
+
|
|
63
|
+
export function isDisplayMode(value: unknown): value is DisplayMode {
|
|
64
|
+
return typeof value === "string" && DISPLAY_MODES.includes(value as DisplayMode);
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
export type SettingsScope = "global" | "project";
|
|
@@ -52,10 +69,11 @@ export type SettingsScope = "global" | "project";
|
|
|
52
69
|
/**
|
|
53
70
|
* How a judgment that lands in the middle band is resolved.
|
|
54
71
|
*
|
|
55
|
-
* `
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
72
|
+
* `allow` (the default) keeps an auto mode useful: Jev blocks what it can clearly
|
|
73
|
+
* reject and lets an unclear answer through, so the gate never interrupts. `deny`
|
|
74
|
+
* is the conservative alternative for anyone who wants "not sure" to stop a call.
|
|
75
|
+
* `ask` hands the call to the user, which contradicts the point of an auto mode and
|
|
76
|
+
* is therefore not the default.
|
|
59
77
|
*/
|
|
60
78
|
export type UncertainAction = "deny" | "ask" | "allow";
|
|
61
79
|
|
|
@@ -86,9 +104,10 @@ export const DEFAULT_SETTINGS: JevAutoModeSettings = {
|
|
|
86
104
|
disallowedCommands: [],
|
|
87
105
|
extraProtectedPaths: [],
|
|
88
106
|
maxStateCharacters: 120_000,
|
|
89
|
-
uncertain: "
|
|
107
|
+
uncertain: "allow",
|
|
90
108
|
gateScope: "all",
|
|
91
109
|
thresholds: {},
|
|
110
|
+
display: "compact",
|
|
92
111
|
};
|
|
93
112
|
|
|
94
113
|
const MAX_PATTERN_ENTRIES = 200;
|
|
@@ -192,6 +211,10 @@ export function parseSettingsPatch(value: unknown): SettingsPatch {
|
|
|
192
211
|
patch.gateScope = record.gateScope;
|
|
193
212
|
}
|
|
194
213
|
|
|
214
|
+
if (record.display !== undefined && isDisplayMode(record.display)) {
|
|
215
|
+
patch.display = record.display;
|
|
216
|
+
}
|
|
217
|
+
|
|
195
218
|
const safeCommands = record.safeCommands === undefined ? undefined : readStringArray(record.safeCommands);
|
|
196
219
|
if (safeCommands !== undefined) patch.safeCommands = safeCommands;
|
|
197
220
|
|
package/src/ui.ts
CHANGED
|
@@ -21,8 +21,7 @@ export interface StatusInput {
|
|
|
21
21
|
export function statusText(input: StatusInput): string {
|
|
22
22
|
if (!input.enabled) return "🛡 jev off";
|
|
23
23
|
const scope = input.scope === "project" ? "project" : "global";
|
|
24
|
-
|
|
25
|
-
return `🛡 jev${engine} (${scope})`;
|
|
24
|
+
return input.engineId === "manual" ? `🛡 jev no key (${scope})` : `🛡 jev (${scope})`;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
export interface StatusContext {
|
|
@@ -45,6 +44,7 @@ export function describeSettings(settings: JevAutoModeSettings, scope: SettingsS
|
|
|
45
44
|
`max state characters: ${settings.maxStateCharacters}`,
|
|
46
45
|
`uncertain band: ${settings.uncertain}`,
|
|
47
46
|
`gate scope: ${settings.gateScope}`,
|
|
47
|
+
`display: ${settings.display}`,
|
|
48
48
|
].join("\n");
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -150,6 +150,15 @@ export const USAGE_TEXT = [
|
|
|
150
150
|
" /jev-auto-mode uncertain show what the middle band resolves to",
|
|
151
151
|
" /jev-auto-mode uncertain deny|ask|allow",
|
|
152
152
|
" /jev-auto-mode scope all|matched which calls reach Jev",
|
|
153
|
+
" /jev-auto-mode display full|compact how decision records are drawn",
|
|
154
|
+
].join("\n");
|
|
155
|
+
|
|
156
|
+
export const DISPLAY_EXPLANATION = [
|
|
157
|
+
"How decision records are drawn in the transcript.",
|
|
158
|
+
" compact - one line for an approval, two for a block (with the rationale) (default).",
|
|
159
|
+
" full - heading, engine line, command, reasons, and rationale.",
|
|
160
|
+
"The expanded view always shows every detail. New records use the mode at once;",
|
|
161
|
+
"records already on screen switch when Pi redraws them (e.g. toggling expansion).",
|
|
153
162
|
].join("\n");
|
|
154
163
|
|
|
155
164
|
export const GATE_SCOPE_EXPLANATION = [
|