@hank-warren/pi-auto-permissions 0.16.0 → 0.16.2

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.
@@ -0,0 +1,264 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { AutoPermissionsConfig } from "./config.js";
3
+ import type { PromptChoiceClassification } from "./evaluation-log.js";
4
+ import type { PermissionOverride } from "./override-evidence.js";
5
+ import type { ReviewScope } from "./review-scope.js";
6
+ import type { DenialSummary } from "./settings-menu.js";
7
+ import {
8
+ grantStandingApproval,
9
+ readStandingApprovals,
10
+ STANDING_APPROVAL_LIMIT,
11
+ standingApprovalsToPermissionOverrides,
12
+ } from "./standing-overrides.js";
13
+
14
+ /**
15
+ * Overrides persist as custom session entries the way the loop budget does,
16
+ * so a resumed session keeps the user's prompt decisions instead of
17
+ * forgetting every allow and standing block constraint.
18
+ */
19
+ const OVERRIDES_ENTRY_TYPE = "auto-permissions-overrides";
20
+
21
+ export interface SessionOverrides {
22
+ /** Commands a `request_override` grant cleared for the rest of this session. */
23
+ readonly allowedConventionCommands: ReadonlySet<string>;
24
+ /** The override records, live, for merging into reviewer evidence. */
25
+ list(): readonly PermissionOverride[];
26
+ allowConvention(command: string): void;
27
+ activateOverrideTool(): void;
28
+ reconcileOverrideTool(): void;
29
+ persist(): void;
30
+ restore(branch: readonly unknown[]): void;
31
+ removeStanding(matches: (override: PermissionOverride) => boolean): void;
32
+ loadStanding(config: AutoPermissionsConfig, ctx: ExtensionContext): void;
33
+ recordPromptDecision(
34
+ scope: ReviewScope,
35
+ classification: PromptChoiceClassification,
36
+ detail: string,
37
+ anchorKey: string | undefined,
38
+ ): void;
39
+ allowRetry(denial: DenialSummary, anchorKey: string | undefined): void;
40
+ resetForSession(): void;
41
+ }
42
+
43
+ /**
44
+ * The user's own permission decisions for this session: the exception grants
45
+ * that let a convention-blocked command run, and the override records the
46
+ * guardian is shown as user-source evidence.
47
+ *
48
+ * One owner, because the pieces are one fact seen three ways — the in-memory
49
+ * record, the session entry that survives a resume, and (for standing
50
+ * approvals) the cross-project ledger.
51
+ */
52
+ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
53
+ const allowedConventionCommands = new Set<string>();
54
+ const permissionOverrides: PermissionOverride[] = [];
55
+ let overrideSeq = 0;
56
+ let standingApprovalCapNotified = false;
57
+ let overrideToolActivated = false;
58
+
59
+ function reconcileOverrideTool(): void {
60
+ if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
61
+ const active = pi.getActiveTools();
62
+ const present = active.includes("request_override");
63
+ if (overrideToolActivated === present) return;
64
+ pi.setActiveTools(
65
+ overrideToolActivated
66
+ ? [...active, "request_override"]
67
+ : active.filter((name) => name !== "request_override"),
68
+ );
69
+ }
70
+
71
+ function persist(): void {
72
+ try {
73
+ pi.appendEntry(OVERRIDES_ENTRY_TYPE, {
74
+ seq: overrideSeq,
75
+ overrides: permissionOverrides
76
+ .filter((override) => !override.standing)
77
+ .map((override) => ({ ...override })),
78
+ });
79
+ } catch {
80
+ // Persistence is best-effort; the in-memory records still apply now.
81
+ }
82
+ }
83
+
84
+ /** Fail-open like the loop budget: unreadable state means no overrides. */
85
+ function restoreOverrides(entries: readonly unknown[]): void {
86
+ permissionOverrides.length = 0;
87
+ overrideSeq = 0;
88
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
89
+ const entry = entries[index] as { type?: string; customType?: string; data?: unknown } | undefined;
90
+ if (entry?.type !== "custom" || entry.customType !== OVERRIDES_ENTRY_TYPE) continue;
91
+ const data = entry.data as { seq?: unknown; overrides?: unknown } | undefined;
92
+ if (Array.isArray(data?.overrides)) {
93
+ for (const raw of data.overrides) {
94
+ if (!raw || typeof raw !== "object") continue;
95
+ const candidate = raw as PermissionOverride;
96
+ if (
97
+ typeof candidate.seq !== "number"
98
+ || typeof candidate.gateLabel !== "string"
99
+ || typeof candidate.command !== "string"
100
+ || typeof candidate.reviewerReason !== "string"
101
+ || !["allow", "allow_unnecessary", "allow_appropriate", "block"].includes(candidate.choice)
102
+ ) {
103
+ continue;
104
+ }
105
+ permissionOverrides.push({
106
+ seq: candidate.seq,
107
+ ...(typeof candidate.anchorKey === "string" ? { anchorKey: candidate.anchorKey } : {}),
108
+ gateLabel: candidate.gateLabel,
109
+ command: candidate.command,
110
+ reviewerReason: candidate.reviewerReason,
111
+ choice: candidate.choice,
112
+ });
113
+ }
114
+ }
115
+ if (typeof data?.seq === "number" && Number.isSafeInteger(data.seq) && data.seq >= 0) {
116
+ overrideSeq = data.seq;
117
+ }
118
+ return;
119
+ }
120
+ }
121
+
122
+ /** Drop matching ledger-backed evidence in place; the array is shared by reference. */
123
+ function removeStanding(matches: (override: PermissionOverride) => boolean): void {
124
+ const kept = permissionOverrides.filter((override) => !override.standing || !matches(override));
125
+ permissionOverrides.splice(0, permissionOverrides.length, ...kept);
126
+ }
127
+
128
+ return {
129
+ allowedConventionCommands,
130
+ list: () => permissionOverrides,
131
+ allowConvention(command: string): void {
132
+ allowedConventionCommands.add(command);
133
+ },
134
+ reconcileOverrideTool,
135
+ activateOverrideTool(): void {
136
+ overrideToolActivated = true;
137
+ reconcileOverrideTool();
138
+ },
139
+ persist,
140
+ removeStanding,
141
+
142
+ restore(branch: readonly unknown[]): void {
143
+ // Restore prompt decisions from the branch: a resumed session keeps its
144
+ // allows and standing block constraints (parity with the loop budget).
145
+ restoreOverrides(branch);
146
+ for (const entry of branch as Array<{
147
+ type?: string;
148
+ message?: { role?: string; toolName?: string; details?: unknown };
149
+ }>) {
150
+ if (entry.type !== "message" || entry.message?.role !== "toolResult") continue;
151
+ if (entry.message.toolName !== "request_override") continue;
152
+ const details = entry.message.details as { success?: boolean; command?: string } | undefined;
153
+ if (details?.success && details.command) allowedConventionCommands.add(details.command);
154
+ }
155
+ },
156
+
157
+ /** Replace only ledger-backed evidence; session prompt decisions stay put. */
158
+ loadStanding(config: AutoPermissionsConfig, ctx: ExtensionContext): void {
159
+ removeStanding(() => true);
160
+ if (!config.standingApprovals.enabled) return;
161
+ try {
162
+ const records = readStandingApprovals(config.standingApprovals.path);
163
+ permissionOverrides.push(...standingApprovalsToPermissionOverrides(records));
164
+ } catch (error) {
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ ctx.ui.notify(`Could not load standing Auto Permissions approvals: ${message}`, "warning");
167
+ }
168
+ },
169
+
170
+ /**
171
+ * Feed the user's decision back to the guardian as session-scoped
172
+ * user-source evidence. Only the caller knows whether the prompt came from
173
+ * a guardian judgment, which is the one case this may be called for.
174
+ */
175
+ recordPromptDecision(
176
+ scope: ReviewScope,
177
+ classification: PromptChoiceClassification,
178
+ detail: string,
179
+ anchorKey: string | undefined,
180
+ ): void {
181
+ const { ctx, config, gate, command } = scope;
182
+ const overrideChoice = classification.userChoice ?? (classification.allowsExecution ? "allow" as const : undefined);
183
+ if (!overrideChoice) return;
184
+ let recordedStanding = false;
185
+ if (classification.standingApproval && config.standingApprovals.enabled) {
186
+ try {
187
+ const { evicted, override } = grantStandingApproval(
188
+ config.standingApprovals.path,
189
+ {
190
+ gate: { label: gate.label, group: gate.group },
191
+ command,
192
+ project: ctx.cwd,
193
+ reason: detail,
194
+ },
195
+ overrideSeq++,
196
+ anchorKey,
197
+ );
198
+ removeStanding(
199
+ (existing) => existing.standing?.gateGroup === gate.group && existing.command === command,
200
+ );
201
+ permissionOverrides.push(override);
202
+ recordedStanding = true;
203
+ if (evicted > 0 && !standingApprovalCapNotified) {
204
+ standingApprovalCapNotified = true;
205
+ ctx.ui.notify(
206
+ `Standing approvals reached ${STANDING_APPROVAL_LIMIT} entries; the oldest approval was removed.`,
207
+ "warning",
208
+ );
209
+ }
210
+ } catch (error) {
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ ctx.ui.notify(`Could not save standing approval: ${message}`, "warning");
213
+ }
214
+ }
215
+ if (!recordedStanding) {
216
+ permissionOverrides.push({
217
+ seq: overrideSeq++,
218
+ anchorKey,
219
+ gateLabel: gate.label,
220
+ command,
221
+ reviewerReason: detail,
222
+ choice: overrideChoice,
223
+ });
224
+ }
225
+ persist();
226
+ },
227
+
228
+ /**
229
+ * "Allow on retry": the existing override machinery, driven from the
230
+ * denial ledger. An exact-command allow override (which already
231
+ * generalizes correctly and survives via evidence re-injection and the
232
+ * session entry), plus a visible injected message telling the agent it
233
+ * may retry. No new authorization pathway.
234
+ */
235
+ allowRetry(denial: DenialSummary, anchorKey: string | undefined): void {
236
+ permissionOverrides.push({
237
+ seq: overrideSeq++,
238
+ anchorKey,
239
+ gateLabel: denial.gateLabel,
240
+ command: denial.command,
241
+ reviewerReason: denial.reason,
242
+ choice: "allow",
243
+ });
244
+ persist();
245
+ try {
246
+ // The same channel a prompt note uses. This is a real user decision
247
+ // made in the menu, so a user message is honest provenance — and it
248
+ // gives the agent a turn to actually retry.
249
+ pi.sendUserMessage(
250
+ `Auto Permissions: I reviewed the denied command in /auto-permissions and allowed it on retry:\n\n ${denial.command}\n\nYou may run this exact command again; a session override now authorizes it.`,
251
+ );
252
+ } catch {
253
+ // The override itself is already in force; the nudge is best-effort.
254
+ }
255
+ },
256
+
257
+ resetForSession(): void {
258
+ overrideToolActivated = false;
259
+ reconcileOverrideTool();
260
+ standingApprovalCapNotified = false;
261
+ allowedConventionCommands.clear();
262
+ },
263
+ };
264
+ }
@@ -0,0 +1,261 @@
1
+ import {
2
+ getSelectListTheme,
3
+ getSettingsListTheme,
4
+ type ExtensionAPI,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { SettingsList } from "@earendil-works/pi-tui";
7
+ import {
8
+ autoPermissionsConfigPath,
9
+ loadAutoPermissionsConfig,
10
+ type AutoPermissionsConfig,
11
+ } from "./config.js";
12
+ import { patchAutoPermissionsConfig } from "./config-writer.js";
13
+ import { readRecentDenials } from "./denial-log.js";
14
+ import type { GuardianReviewer } from "./guardian-reviewer.js";
15
+ import type { SessionOverrides } from "./session-overrides.js";
16
+ import {
17
+ applySettingChange,
18
+ buildSettingItems,
19
+ createDenialsSubmenu,
20
+ createModelSubmenu,
21
+ createStandingApprovalsSubmenu,
22
+ createTimeoutSubmenu,
23
+ type DenialsSubmenuHost,
24
+ type DenialSummary,
25
+ type StandingApprovalsSubmenuHost,
26
+ type StandingApprovalSummary,
27
+ type MenuModel,
28
+ type ReviewerSettings,
29
+ type SettingChange,
30
+ type SubmenuHost,
31
+ } from "./settings-menu.js";
32
+ import { readStandingApprovals, revokeStandingApproval } from "./standing-overrides.js";
33
+
34
+ /**
35
+ * What `/auto-permissions setup` says on the user's behalf. Named after the
36
+ * bundled skill rather than a `/skill:` invocation, because skill commands can
37
+ * be turned off in settings while the skill itself stays discoverable.
38
+ */
39
+ const SETUP_SKILL_HANDOFF_MESSAGE =
40
+ "Use the auto-permissions-setup skill to set up my Auto Permissions policy.";
41
+
42
+ export function registerSettingsCommand(
43
+ pi: ExtensionAPI,
44
+ { overrides, reviewer }: { overrides: SessionOverrides; reviewer: GuardianReviewer },
45
+ ): void {
46
+ /**
47
+ * `/auto-permissions`: edit the reviewer model, thinking level, timeout and
48
+ * the enabled flag without hand-editing config.json.
49
+ *
50
+ * There is no live-apply step: every tool call re-reads the config through
51
+ * currentConfig(), so a saved edit is in force immediately -- in this session
52
+ * and in every other one. reviewerFingerprint() covers provider, model and
53
+ * reasoning effort, so the cached reviewer lineage self-invalidates too.
54
+ *
55
+ * `/auto-permissions setup` is a pointer, not a wizard: it hands the job to
56
+ * the bundled auto-permissions-setup skill. A conversational setup can ask
57
+ * about an ambiguous host, weigh history-wide friction and back out of a bad
58
+ * suggestion; a fixed accept-or-discard draft could do none of that, so the
59
+ * two paths were never equal and keeping both only split the maintenance.
60
+ */
61
+ pi.registerCommand("auto-permissions", {
62
+ description: "Configure Auto Permissions (\"setup\" hands off to the auto-permissions-setup skill)",
63
+ handler: async (args, ctx) => {
64
+ if ((args ?? "").trim() === "setup") {
65
+ // Dispatch rather than instruct: the point of the pointer is that the
66
+ // interview starts on this keystroke.
67
+ pi.sendUserMessage(SETUP_SKILL_HANDOFF_MESSAGE);
68
+ return;
69
+ }
70
+ if (ctx.mode !== "tui") {
71
+ ctx.ui.notify("/auto-permissions requires interactive TUI mode", "warning");
72
+ return;
73
+ }
74
+
75
+ const path = autoPermissionsConfigPath();
76
+ let settings: ReviewerSettings;
77
+ let denialLog: AutoPermissionsConfig["denialLog"];
78
+ let standingApprovalsConfig: AutoPermissionsConfig["standingApprovals"];
79
+ try {
80
+ const config = loadAutoPermissionsConfig(path);
81
+ settings = {
82
+ enabled: config.enabled,
83
+ reviewer: config.reviewer,
84
+ systemPromptSource: config.systemPromptSource,
85
+ };
86
+ denialLog = config.denialLog;
87
+ standingApprovalsConfig = config.standingApprovals;
88
+ } catch (error) {
89
+ // Editing a file we cannot validate is the one way this writer could
90
+ // quietly make things worse, so refuse rather than open.
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ ctx.ui.notify(`Auto Permissions config error: ${message} \u2014 fix ${path} first`, "warning");
93
+ return;
94
+ }
95
+
96
+ /** Persist one accepted edit, then report anything the save surfaced. */
97
+ const commit = (change: SettingChange): void => {
98
+ if (change.kind === "ignored") return;
99
+ if (change.kind === "error") {
100
+ ctx.ui.notify(change.message, "warning");
101
+ return;
102
+ }
103
+ const previous = settings;
104
+ settings = change.settings;
105
+ try {
106
+ patchAutoPermissionsConfig(path, { enabled: settings.enabled, reviewer: settings.reviewer });
107
+ } catch (error) {
108
+ settings = previous;
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ ctx.ui.notify(`Could not save Auto Permissions settings: ${message}`, "warning");
111
+ return;
112
+ }
113
+ if (change.kind === "warn") ctx.ui.notify(change.message, "warning");
114
+ // A breakage elsewhere in the file would otherwise surface at the next
115
+ // bash call, looking like this edit caused it.
116
+ try {
117
+ loadAutoPermissionsConfig(path);
118
+ } catch (error) {
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ ctx.ui.notify(`Auto Permissions config error: ${message}`, "warning");
121
+ }
122
+ };
123
+
124
+ const reviewerModel = (): MenuModel | undefined => {
125
+ if (!settings.reviewer) return undefined;
126
+ return ctx.modelRegistry.find(settings.reviewer.provider, settings.reviewer.model) as MenuModel | undefined;
127
+ };
128
+
129
+ const recentDenials = (): DenialSummary[] => {
130
+ if (!denialLog.enabled) return [];
131
+ try {
132
+ return readRecentDenials(denialLog.path, 20).map((record) => ({
133
+ id: record.id,
134
+ ts: record.ts,
135
+ gateLabel: record.gate.label,
136
+ command: record.command,
137
+ verdict: record.verdict,
138
+ reason: record.reason,
139
+ }));
140
+ } catch {
141
+ return [];
142
+ }
143
+ };
144
+
145
+ const standingApprovalSummaries = (): StandingApprovalSummary[] => {
146
+ if (!standingApprovalsConfig.enabled) return [];
147
+ try {
148
+ return readStandingApprovals(standingApprovalsConfig.path).map((record, index) => ({
149
+ id: `${index}:${record.ts}:${record.gate.label}`,
150
+ record,
151
+ }));
152
+ } catch {
153
+ return [];
154
+ }
155
+ };
156
+
157
+ const revokeApproval = (approval: StandingApprovalSummary): void => {
158
+ try {
159
+ if (!revokeStandingApproval(standingApprovalsConfig.path, approval.record)) {
160
+ ctx.ui.notify("That standing approval is no longer in the ledger.", "warning");
161
+ return;
162
+ }
163
+ const config = loadAutoPermissionsConfig(path);
164
+ overrides.loadStanding(config, ctx);
165
+ reviewer.discardLineage();
166
+ ctx.ui.notify("Standing approval revoked.", "info");
167
+ } catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ ctx.ui.notify(`Could not revoke standing approval: ${message}`, "warning");
170
+ }
171
+ };
172
+
173
+ /**
174
+ * Closes the settings dialog. Assigned when `ctx.ui.custom` builds the
175
+ * component, so it is only ever called from inside the dialog it closes.
176
+ */
177
+ let closeSettings: (() => void) | undefined;
178
+
179
+ /**
180
+ * "Allow on retry": the existing override machinery, driven from the
181
+ * denial ledger. An exact-command allow override (which already
182
+ * generalizes correctly and survives via evidence re-injection and the
183
+ * session entry), plus a visible injected message telling the agent it
184
+ * may retry. No new authorization pathway.
185
+ *
186
+ * The dialog is closed *before* the message is dispatched, and the order
187
+ * is load-bearing. The retry hits the same gate, so the approval prompt
188
+ * opens another `ctx.ui.custom` — stacked on a still-open settings
189
+ * dialog, that orphans this one: it stops rendering but keeps consuming
190
+ * editor input, and the composer never submits again. Observed live
191
+ * twice; the only escape was restarting pi.
192
+ */
193
+ const allowRetry = (denial: DenialSummary): void => {
194
+ closeSettings?.();
195
+ overrides.allowRetry(denial, reviewer.lastEvidenceKey);
196
+ ctx.ui.notify("Override added for the exact command; the agent may retry it.", "info");
197
+ };
198
+
199
+ await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
200
+ closeSettings = () => done(undefined);
201
+ const host: SubmenuHost = {
202
+ getSettings: () => settings,
203
+ commit,
204
+ availableModels: () => ctx.modelRegistry.getAvailable() as MenuModel[],
205
+ requestRender: () => tui.requestRender(),
206
+ settingsTheme: getSettingsListTheme(),
207
+ selectTheme: getSelectListTheme(),
208
+ };
209
+ const denialsHost: DenialsSubmenuHost = {
210
+ recentDenials,
211
+ allowRetry,
212
+ settingsTheme: getSettingsListTheme(),
213
+ selectTheme: getSelectListTheme(),
214
+ requestRender: () => tui.requestRender(),
215
+ };
216
+ const standingHost: StandingApprovalsSubmenuHost = {
217
+ standingApprovals: standingApprovalSummaries,
218
+ revokeStandingApproval: revokeApproval,
219
+ settingsTheme: getSettingsListTheme(),
220
+ selectTheme: getSelectListTheme(),
221
+ requestRender: () => tui.requestRender(),
222
+ };
223
+ const list = new SettingsList(
224
+ buildSettingItems(
225
+ settings,
226
+ {
227
+ reviewerModel: createModelSubmenu(host),
228
+ timeout: createTimeoutSubmenu(host),
229
+ ...(denialLog.enabled ? { recentDenials: createDenialsSubmenu(denialsHost) } : {}),
230
+ ...(standingApprovalsConfig.enabled
231
+ ? { standingApprovals: createStandingApprovalsSubmenu(standingHost) }
232
+ : {}),
233
+ },
234
+ undefined,
235
+ denialLog.enabled ? recentDenials().length : undefined,
236
+ standingApprovalsConfig.enabled ? standingApprovalSummaries().length : undefined,
237
+ ),
238
+ 10,
239
+ host.settingsTheme,
240
+ (id, value) => {
241
+ commit(applySettingChange(settings, id, value, reviewerModel()));
242
+ tui.requestRender();
243
+ },
244
+ () => done(undefined),
245
+ { enableSearch: true },
246
+ );
247
+ return {
248
+ invalidate(): void {
249
+ list.invalidate();
250
+ },
251
+ handleInput(data: string): void {
252
+ list.handleInput(data);
253
+ },
254
+ render(width: number): string[] {
255
+ return list.render(width);
256
+ },
257
+ };
258
+ });
259
+ },
260
+ });
261
+ }
package/settings-menu.ts CHANGED
@@ -29,11 +29,11 @@ import {
29
29
  import type { ReviewerBlock } from "./config-writer.js";
30
30
  import type { StandingApprovalRecord } from "./standing-overrides.js";
31
31
 
32
- export const ENABLED_ID = "enabled";
33
- export const REVIEWER_MODEL_ID = "reviewerModel";
34
- export const THINKING_LEVEL_ID = "reasoningEffort";
35
- export const TIMEOUT_ID = "timeoutMs";
36
- export const SYSTEM_PROMPT_ID = "systemPrompt";
32
+ const ENABLED_ID = "enabled";
33
+ const REVIEWER_MODEL_ID = "reviewerModel";
34
+ const THINKING_LEVEL_ID = "reasoningEffort";
35
+ const TIMEOUT_ID = "timeoutMs";
36
+ const SYSTEM_PROMPT_ID = "systemPrompt";
37
37
  export const RECENT_DENIALS_ID = "recentDenials";
38
38
  export const STANDING_APPROVALS_ID = "standingApprovals";
39
39
 
@@ -41,7 +41,7 @@ export const ON = "on";
41
41
  export const OFF = "off";
42
42
  const TOGGLE_VALUES = [ON, OFF];
43
43
 
44
- export const UNSET_MODEL = "(unset)";
44
+ const UNSET_MODEL = "(unset)";
45
45
  const UNAVAILABLE_SUFFIX = "(configured, unavailable)";
46
46
  const NO_REVIEWER_MESSAGE = "Select a reviewer model first";
47
47
 
@@ -66,13 +66,13 @@ export function modelKey(model: { provider: string; id: string }): string {
66
66
  }
67
67
 
68
68
  /** Inverse of {@link modelKey}; the provider itself never contains a slash. */
69
- export function parseModelKey(value: string): { provider: string; model: string } | undefined {
69
+ function parseModelKey(value: string): { provider: string; model: string } | undefined {
70
70
  const slash = value.indexOf("/");
71
71
  if (slash <= 0 || slash === value.length - 1) return undefined;
72
72
  return { provider: value.slice(0, slash), model: value.slice(slash + 1) };
73
73
  }
74
74
 
75
- export function reviewerValue(settings: ReviewerSettings): string {
75
+ function reviewerValue(settings: ReviewerSettings): string {
76
76
  return settings.reviewer ? modelKey({ provider: settings.reviewer.provider, id: settings.reviewer.model }) : UNSET_MODEL;
77
77
  }
78
78
 
@@ -80,11 +80,11 @@ export function toggleValue(enabled: boolean): string {
80
80
  return enabled ? ON : OFF;
81
81
  }
82
82
 
83
- export function thinkingValue(settings: ReviewerSettings): ReasoningEffort {
83
+ function thinkingValue(settings: ReviewerSettings): ReasoningEffort {
84
84
  return settings.reviewer?.reasoningEffort ?? DEFAULT_REVIEWER_REASONING_EFFORT;
85
85
  }
86
86
 
87
- export function timeoutValue(settings: ReviewerSettings): number {
87
+ function timeoutValue(settings: ReviewerSettings): number {
88
88
  return settings.reviewer?.timeoutMs ?? DEFAULT_REVIEWER_TIMEOUT_MS;
89
89
  }
90
90
 
@@ -117,7 +117,7 @@ export function formatTimeout(timeoutMs: number): string {
117
117
  return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
118
118
  }
119
119
 
120
- export type TimeoutParse = { timeoutMs: number } | { error: string };
120
+ type TimeoutParse = { timeoutMs: number } | { error: string };
121
121
 
122
122
  /**
123
123
  * Parse a timeout the user typed. A bare number means seconds, because that is
@@ -13,7 +13,7 @@ import { dirname } from "node:path";
13
13
  import { pid } from "node:process";
14
14
  import { OVERRIDE_ANCHOR_START, type PermissionOverride } from "./override-evidence.js";
15
15
 
16
- export const STANDING_APPROVAL_SCOPE = "comparable" as const;
16
+ const STANDING_APPROVAL_SCOPE = "comparable" as const;
17
17
  export const STANDING_APPROVAL_LIMIT = 200;
18
18
  const STALE_LEDGER_LOCK_MS = 60_000;
19
19
 
@@ -30,7 +30,7 @@ export interface StandingApprovalRecord {
30
30
  reason: string;
31
31
  }
32
32
 
33
- export interface StandingApprovalInput {
33
+ interface StandingApprovalInput {
34
34
  gate: StandingApprovalRecord["gate"];
35
35
  command: string;
36
36
  project: string;
@@ -200,7 +200,7 @@ export function appendStandingApproval(
200
200
  });
201
201
  }
202
202
 
203
- export function standingApprovalToPermissionOverride(
203
+ function standingApprovalToPermissionOverride(
204
204
  record: StandingApprovalRecord,
205
205
  seq: number,
206
206
  anchorKey: string = OVERRIDE_ANCHOR_START,
package/usage-log.ts CHANGED
@@ -5,7 +5,7 @@ import { dirname } from "node:path";
5
5
  /** Rotate once the sidecar grows past this size; one previous generation is kept. */
6
6
  export const USAGE_LOG_ROTATE_BYTES = 16 * 1024 * 1024;
7
7
 
8
- export interface UsageLogTotals {
8
+ interface UsageLogTotals {
9
9
  input: number;
10
10
  output: number;
11
11
  cacheRead: number;
package/widget-status.ts CHANGED
@@ -12,9 +12,9 @@ export type ReviewDisplayState =
12
12
  | "ask_user"
13
13
  | "blocked";
14
14
 
15
- export type ReviewStatusTone = "muted" | "warning" | "success" | "accent" | "error";
15
+ type ReviewStatusTone = "muted" | "warning" | "success" | "accent" | "error";
16
16
 
17
- export interface ReviewStatusFrame {
17
+ interface ReviewStatusFrame {
18
18
  glyph: string;
19
19
  label: string;
20
20
  tone: ReviewStatusTone;