@hank-warren/pi-auto-permissions 0.17.0 → 0.19.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 +27 -0
- package/README.md +47 -3
- package/config-writer.ts +8 -2
- package/config.ts +16 -0
- package/denial-log.ts +8 -0
- package/guardian-reviewer.ts +132 -49
- package/guardian-transport.ts +5 -6
- package/index.ts +531 -61
- package/package.json +1 -1
- package/prompt-select.ts +5 -0
- package/review-display.ts +133 -58
- package/review-queue.ts +88 -11
- package/review-scope.ts +11 -0
- package/review.ts +118 -12
- package/settings-command.ts +12 -4
- package/settings-menu.ts +29 -0
- package/subagent-context.ts +6 -6
- package/widget-status.ts +41 -4
package/settings-command.ts
CHANGED
|
@@ -40,8 +40,8 @@ export function registerSettingsCommand(
|
|
|
40
40
|
{ overrides, reviewer }: { overrides: SessionOverrides; reviewer: GuardianReviewer },
|
|
41
41
|
): void {
|
|
42
42
|
/**
|
|
43
|
-
* `/auto-permissions`: edit the reviewer model, thinking level, timeout
|
|
44
|
-
* the enabled flag without hand-editing config.json.
|
|
43
|
+
* `/auto-permissions`: edit the reviewer model, thinking level, timeout,
|
|
44
|
+
* parallel reviews and the enabled flag without hand-editing config.json.
|
|
45
45
|
*
|
|
46
46
|
* There is no live-apply step: every tool call re-reads the config through
|
|
47
47
|
* currentConfig(), so a saved edit is in force immediately -- in this session
|
|
@@ -76,6 +76,7 @@ export function registerSettingsCommand(
|
|
|
76
76
|
settings = {
|
|
77
77
|
enabled: config.enabled,
|
|
78
78
|
reviewer: config.reviewer,
|
|
79
|
+
reviewConcurrency: config.reviewConcurrency,
|
|
79
80
|
systemPromptSource: config.systemPromptSource,
|
|
80
81
|
};
|
|
81
82
|
denialLog = config.denialLog;
|
|
@@ -97,7 +98,14 @@ export function registerSettingsCommand(
|
|
|
97
98
|
const previous = settings;
|
|
98
99
|
settings = change.settings;
|
|
99
100
|
try {
|
|
100
|
-
patchAutoPermissionsConfig(path, {
|
|
101
|
+
patchAutoPermissionsConfig(path, {
|
|
102
|
+
enabled: settings.enabled,
|
|
103
|
+
reviewer: settings.reviewer,
|
|
104
|
+
// Only when this edit changed it, so a hand-written value is never rewritten by an unrelated save.
|
|
105
|
+
...(settings.reviewConcurrency !== previous.reviewConcurrency
|
|
106
|
+
? { reviewConcurrency: settings.reviewConcurrency }
|
|
107
|
+
: {}),
|
|
108
|
+
});
|
|
101
109
|
} catch (error) {
|
|
102
110
|
settings = previous;
|
|
103
111
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -128,7 +136,7 @@ export function registerSettingsCommand(
|
|
|
128
136
|
ts: record.ts,
|
|
129
137
|
gateLabel: record.gate.label,
|
|
130
138
|
command: record.command,
|
|
131
|
-
verdict: record.verdict,
|
|
139
|
+
verdict: record.reviseFirst ? "revise first" : record.verdict,
|
|
132
140
|
reason: record.reason,
|
|
133
141
|
}));
|
|
134
142
|
} catch {
|
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/subagent-context.ts
CHANGED
|
@@ -4,16 +4,16 @@ import { isAbsolute, join } from "node:path";
|
|
|
4
4
|
/**
|
|
5
5
|
* Runtime facts about a subagent child session, forwarded to the guardian as
|
|
6
6
|
* evidence. Facts only — the guardian judges what they imply; see
|
|
7
|
-
*
|
|
7
|
+
* subagentContextSystemPrompt() in review.ts.
|
|
8
8
|
*
|
|
9
|
-
* Detection is based on the environment contract
|
|
10
|
-
* (nicobailon/pi-subagents)
|
|
11
|
-
* plus run id and nesting depth. Absent that env,
|
|
12
|
-
* ordinary sessions and review behavior is unchanged.
|
|
9
|
+
* Detection is based on the environment contract that `@hank-warren/pi-agents`
|
|
10
|
+
* and `pi-subagents` (nicobailon/pi-subagents) both use: children are spawned
|
|
11
|
+
* with `PI_SUBAGENT_CHILD=1` plus run id and nesting depth. Absent that env,
|
|
12
|
+
* sessions are treated as ordinary sessions and review behavior is unchanged.
|
|
13
13
|
*/
|
|
14
14
|
export interface SubagentExecutionContext {
|
|
15
15
|
subagent: true;
|
|
16
|
-
/**
|
|
16
|
+
/** The subagent's run id (PI_SUBAGENT_RUN_ID), when present. */
|
|
17
17
|
runId?: string;
|
|
18
18
|
/** Nesting depth for nested subagents (PI_SUBAGENT_DEPTH), when > 0. */
|
|
19
19
|
depth?: number;
|
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
|
+
}
|