@hank-warren/pi-auto-permissions 0.17.0 → 0.18.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 +15 -0
- package/README.md +35 -0
- package/config-writer.ts +8 -2
- package/config.ts +16 -0
- package/guardian-reviewer.ts +130 -47
- package/index.ts +446 -64
- package/package.json +1 -1
- package/review-display.ts +133 -58
- package/review-queue.ts +88 -11
- package/review-scope.ts +11 -0
- package/review.ts +104 -9
- package/settings-command.ts +11 -3
- package/settings-menu.ts +29 -0
- package/widget-status.ts +41 -4
package/settings-menu.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
DEFAULT_REVIEWER_REASONING_EFFORT,
|
|
22
22
|
DEFAULT_REVIEWER_TIMEOUT_MS,
|
|
23
|
+
MAX_REVIEW_CONCURRENCY,
|
|
23
24
|
MAX_REVIEWER_TIMEOUT_MS,
|
|
24
25
|
MIN_REVIEWER_TIMEOUT_MS,
|
|
25
26
|
REASONING_EFFORTS,
|
|
@@ -32,6 +33,7 @@ const ENABLED_ID = "enabled";
|
|
|
32
33
|
const REVIEWER_MODEL_ID = "reviewerModel";
|
|
33
34
|
const THINKING_LEVEL_ID = "reasoningEffort";
|
|
34
35
|
const TIMEOUT_ID = "timeoutMs";
|
|
36
|
+
const REVIEW_CONCURRENCY_ID = "reviewConcurrency";
|
|
35
37
|
const SYSTEM_PROMPT_ID = "systemPrompt";
|
|
36
38
|
export const RECENT_DENIALS_ID = "recentDenials";
|
|
37
39
|
|
|
@@ -47,9 +49,20 @@ const NO_REVIEWER_MESSAGE = "Select a reviewer model first";
|
|
|
47
49
|
export interface ReviewerSettings {
|
|
48
50
|
enabled: boolean;
|
|
49
51
|
reviewer?: ReviewerConfig;
|
|
52
|
+
reviewConcurrency: number;
|
|
50
53
|
systemPromptSource: SystemPromptSource;
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
const CONCURRENCY_PRESETS = [1, 2, 4, 8, 16];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The values the parallel-reviews row cycles through. A hand-written value
|
|
60
|
+
* outside the presets is kept in the cycle, so opening the menu never moves it.
|
|
61
|
+
*/
|
|
62
|
+
export function concurrencyValues(current: number): string[] {
|
|
63
|
+
return [...new Set([...CONCURRENCY_PRESETS, current])].sort((a, b) => a - b).map(String);
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
/** The subset of a pi model the menu needs; keeps tests free of pi-ai types. */
|
|
54
67
|
export interface MenuModel {
|
|
55
68
|
provider: string;
|
|
@@ -177,6 +190,13 @@ export function buildSettingItems(
|
|
|
177
190
|
currentValue: formatTimeout(timeoutValue(settings)),
|
|
178
191
|
...(submenus.timeout ? { submenu: submenus.timeout } : {}),
|
|
179
192
|
},
|
|
193
|
+
{
|
|
194
|
+
id: REVIEW_CONCURRENCY_ID,
|
|
195
|
+
label: "Parallel reviews",
|
|
196
|
+
description: "How many guardian reviews may run at once. 1 reviews one command at a time, none ahead of its turn.",
|
|
197
|
+
currentValue: String(settings.reviewConcurrency),
|
|
198
|
+
values: concurrencyValues(settings.reviewConcurrency),
|
|
199
|
+
},
|
|
180
200
|
{
|
|
181
201
|
id: SYSTEM_PROMPT_ID,
|
|
182
202
|
label: "System prompt",
|
|
@@ -241,6 +261,15 @@ export function applySettingChange(
|
|
|
241
261
|
return { kind: "settings", settings: next };
|
|
242
262
|
}
|
|
243
263
|
|
|
264
|
+
if (id === REVIEW_CONCURRENCY_ID) {
|
|
265
|
+
const reviewConcurrency = Number(value);
|
|
266
|
+
if (!Number.isInteger(reviewConcurrency) || reviewConcurrency < 1 || reviewConcurrency > MAX_REVIEW_CONCURRENCY) {
|
|
267
|
+
return { kind: "error", message: `Parallel reviews must be a whole number from 1 to ${MAX_REVIEW_CONCURRENCY}` };
|
|
268
|
+
}
|
|
269
|
+
if (reviewConcurrency === settings.reviewConcurrency) return { kind: "ignored" };
|
|
270
|
+
return { kind: "settings", settings: { ...settings, reviewConcurrency } };
|
|
271
|
+
}
|
|
272
|
+
|
|
244
273
|
// The model and timeout submenus commit their own edit and hand back display
|
|
245
274
|
// text, so there is nothing left to map here.
|
|
246
275
|
return { kind: "ignored" };
|
package/widget-status.ts
CHANGED
|
@@ -30,10 +30,10 @@ export function reviewStatusFrame(
|
|
|
30
30
|
): ReviewStatusFrame {
|
|
31
31
|
const index = Math.max(0, Math.floor(frameIndex));
|
|
32
32
|
switch (state) {
|
|
33
|
-
// Static rather than animated: the row is replaced
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// renders *something* instead of a blank gap
|
|
33
|
+
// Static rather than animated: the row is replaced the moment the command
|
|
34
|
+
// gets its slot, so a spinner here would promise progress the command is
|
|
35
|
+
// not making. It exists so a guarded command waiting for a review slot or
|
|
36
|
+
// for another command's decision renders *something* instead of a blank gap.
|
|
37
37
|
case "queued":
|
|
38
38
|
return {
|
|
39
39
|
glyph: "⋯",
|
|
@@ -61,6 +61,19 @@ export function reviewFrameIntervalMs(state: ReviewDisplayState): number | undef
|
|
|
61
61
|
return state === "waiting" ? WAITING_FRAME_INTERVAL_MS : undefined;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** The states a review is still in while it has not settled. */
|
|
65
|
+
export type ActiveReviewState = Extract<ReviewDisplayState, "queued" | "waiting" | "ask_user">;
|
|
66
|
+
|
|
67
|
+
export function isActiveReviewState(state: ReviewDisplayState): state is ActiveReviewState {
|
|
68
|
+
return state === "queued" || state === "waiting" || state === "ask_user";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ReviewSummaryCounts {
|
|
72
|
+
waiting: number;
|
|
73
|
+
queued: number;
|
|
74
|
+
askUser: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
export interface ReviewLinePalette {
|
|
65
78
|
header: (text: string) => string;
|
|
66
79
|
muted: (text: string) => string;
|
|
@@ -91,3 +104,27 @@ export function reviewStatusLines(
|
|
|
91
104
|
if (detail) lines.push(palette.muted(detail));
|
|
92
105
|
return lines;
|
|
93
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One line for several unsettled reviews at once. Per-command detail is left
|
|
110
|
+
* out: each command is visible in its own tool box, and an approval prompt
|
|
111
|
+
* carries its own reason.
|
|
112
|
+
*/
|
|
113
|
+
export function reviewSummaryLines(
|
|
114
|
+
counts: ReviewSummaryCounts,
|
|
115
|
+
reviewer: string,
|
|
116
|
+
frameIndex: number,
|
|
117
|
+
palette: ReviewLinePalette,
|
|
118
|
+
): string[] {
|
|
119
|
+
const total = counts.waiting + counts.queued + counts.askUser;
|
|
120
|
+
const parts: string[] = [];
|
|
121
|
+
if (counts.waiting > 0) {
|
|
122
|
+
const frame = reviewStatusFrame("waiting", reviewer, frameIndex);
|
|
123
|
+
parts.push(palette.warning(`${frame.glyph} ${counts.waiting} ${frame.label}`));
|
|
124
|
+
}
|
|
125
|
+
if (counts.queued > 0) parts.push(palette.muted(`⋯ ${counts.queued} queued`));
|
|
126
|
+
if (counts.askUser > 0) parts.push(palette.accent(`? ${counts.askUser} waiting for your approval`));
|
|
127
|
+
return [
|
|
128
|
+
`${palette.header("auto permissions")} ${palette.muted(`· ${total} commands ·`)} ${parts.join(palette.muted(" · "))}`,
|
|
129
|
+
];
|
|
130
|
+
}
|