@hank-warren/pi-auto-permissions 0.16.2 → 0.17.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.
@@ -1,87 +1,53 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { AutoPermissionsConfig } from "./config.js";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
2
  import type { PromptChoiceClassification } from "./evaluation-log.js";
4
- import type { PermissionOverride } from "./override-evidence.js";
5
- import type { ReviewScope } from "./review-scope.js";
3
+ import type { Gate } from "./gates.js";
4
+ import { PERMISSION_OVERRIDE_CHOICES, type PermissionOverride } from "./override-evidence.js";
6
5
  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
6
 
14
7
  /**
15
- * Overrides persist as custom session entries the way the loop budget does,
8
+ * Overrides persist as custom session entries,
16
9
  * so a resumed session keeps the user's prompt decisions instead of
17
10
  * forgetting every allow and standing block constraint.
18
11
  */
19
12
  const OVERRIDES_ENTRY_TYPE = "auto-permissions-overrides";
20
13
 
21
14
  export interface SessionOverrides {
22
- /** Commands a `request_override` grant cleared for the rest of this session. */
23
- readonly allowedConventionCommands: ReadonlySet<string>;
24
15
  /** The override records, live, for merging into reviewer evidence. */
25
16
  list(): readonly PermissionOverride[];
26
- allowConvention(command: string): void;
27
- activateOverrideTool(): void;
28
- reconcileOverrideTool(): void;
29
- persist(): void;
30
17
  restore(branch: readonly unknown[]): void;
31
- removeStanding(matches: (override: PermissionOverride) => boolean): void;
32
- loadStanding(config: AutoPermissionsConfig, ctx: ExtensionContext): void;
33
18
  recordPromptDecision(
34
- scope: ReviewScope,
19
+ gate: Pick<Gate, "label">,
20
+ command: string,
35
21
  classification: PromptChoiceClassification,
36
22
  detail: string,
37
23
  anchorKey: string | undefined,
38
24
  ): void;
39
25
  allowRetry(denial: DenialSummary, anchorKey: string | undefined): void;
40
- resetForSession(): void;
41
26
  }
42
27
 
43
28
  /**
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.
29
+ * The user's own permission decisions for this session: the override records
30
+ * the guardian is shown as user-source evidence.
47
31
  *
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.
32
+ * One owner, because the pieces are one fact seen two ways — the in-memory
33
+ * record and the session entry that survives a resume.
51
34
  */
52
35
  export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
53
- const allowedConventionCommands = new Set<string>();
54
36
  const permissionOverrides: PermissionOverride[] = [];
55
37
  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
38
 
71
39
  function persist(): void {
72
40
  try {
73
41
  pi.appendEntry(OVERRIDES_ENTRY_TYPE, {
74
42
  seq: overrideSeq,
75
- overrides: permissionOverrides
76
- .filter((override) => !override.standing)
77
- .map((override) => ({ ...override })),
43
+ overrides: permissionOverrides.map((override) => ({ ...override })),
78
44
  });
79
45
  } catch {
80
46
  // Persistence is best-effort; the in-memory records still apply now.
81
47
  }
82
48
  }
83
49
 
84
- /** Fail-open like the loop budget: unreadable state means no overrides. */
50
+ /** Fail-open: unreadable state means no overrides. */
85
51
  function restoreOverrides(entries: readonly unknown[]): void {
86
52
  permissionOverrides.length = 0;
87
53
  overrideSeq = 0;
@@ -98,7 +64,7 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
98
64
  || typeof candidate.gateLabel !== "string"
99
65
  || typeof candidate.command !== "string"
100
66
  || typeof candidate.reviewerReason !== "string"
101
- || !["allow", "allow_unnecessary", "allow_appropriate", "block"].includes(candidate.choice)
67
+ || !PERMISSION_OVERRIDE_CHOICES.includes(candidate.choice)
102
68
  ) {
103
69
  continue;
104
70
  }
@@ -119,52 +85,24 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
119
85
  }
120
86
  }
121
87
 
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);
88
+ function addOverride(
89
+ gateLabel: string,
90
+ command: string,
91
+ reviewerReason: string,
92
+ choice: PermissionOverride["choice"],
93
+ anchorKey: string | undefined,
94
+ ): void {
95
+ permissionOverrides.push({ seq: overrideSeq++, anchorKey, gateLabel, command, reviewerReason, choice });
96
+ persist();
126
97
  }
127
98
 
128
99
  return {
129
- allowedConventionCommands,
130
100
  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
101
 
142
102
  restore(branch: readonly unknown[]): void {
143
103
  // Restore prompt decisions from the branch: a resumed session keeps its
144
- // allows and standing block constraints (parity with the loop budget).
104
+ // allows and standing block constraints.
145
105
  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
106
  },
169
107
 
170
108
  /**
@@ -173,56 +111,15 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
173
111
  * a guardian judgment, which is the one case this may be called for.
174
112
  */
175
113
  recordPromptDecision(
176
- scope: ReviewScope,
114
+ gate: Pick<Gate, "label">,
115
+ command: string,
177
116
  classification: PromptChoiceClassification,
178
117
  detail: string,
179
118
  anchorKey: string | undefined,
180
119
  ): void {
181
- const { ctx, config, gate, command } = scope;
182
120
  const overrideChoice = classification.userChoice ?? (classification.allowsExecution ? "allow" as const : undefined);
183
121
  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();
122
+ addOverride(gate.label, command, detail, overrideChoice, anchorKey);
226
123
  },
227
124
 
228
125
  /**
@@ -233,15 +130,7 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
233
130
  * may retry. No new authorization pathway.
234
131
  */
235
132
  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();
133
+ addOverride(denial.gateLabel, denial.command, denial.reason, "allow", anchorKey);
245
134
  try {
246
135
  // The same channel a prompt note uses. This is a real user decision
247
136
  // made in the menu, so a user message is honest provenance — and it
@@ -253,12 +142,5 @@ export function createSessionOverrides(pi: ExtensionAPI): SessionOverrides {
253
142
  // The override itself is already in force; the nudge is best-effort.
254
143
  }
255
144
  },
256
-
257
- resetForSession(): void {
258
- overrideToolActivated = false;
259
- reconcileOverrideTool();
260
- standingApprovalCapNotified = false;
261
- allowedConventionCommands.clear();
262
- },
263
145
  };
264
146
  }
@@ -18,18 +18,14 @@ import {
18
18
  buildSettingItems,
19
19
  createDenialsSubmenu,
20
20
  createModelSubmenu,
21
- createStandingApprovalsSubmenu,
22
21
  createTimeoutSubmenu,
23
22
  type DenialsSubmenuHost,
24
23
  type DenialSummary,
25
- type StandingApprovalsSubmenuHost,
26
- type StandingApprovalSummary,
27
24
  type MenuModel,
28
25
  type ReviewerSettings,
29
26
  type SettingChange,
30
27
  type SubmenuHost,
31
28
  } from "./settings-menu.js";
32
- import { readStandingApprovals, revokeStandingApproval } from "./standing-overrides.js";
33
29
 
34
30
  /**
35
31
  * What `/auto-permissions setup` says on the user's behalf. Named after the
@@ -75,7 +71,6 @@ export function registerSettingsCommand(
75
71
  const path = autoPermissionsConfigPath();
76
72
  let settings: ReviewerSettings;
77
73
  let denialLog: AutoPermissionsConfig["denialLog"];
78
- let standingApprovalsConfig: AutoPermissionsConfig["standingApprovals"];
79
74
  try {
80
75
  const config = loadAutoPermissionsConfig(path);
81
76
  settings = {
@@ -84,7 +79,6 @@ export function registerSettingsCommand(
84
79
  systemPromptSource: config.systemPromptSource,
85
80
  };
86
81
  denialLog = config.denialLog;
87
- standingApprovalsConfig = config.standingApprovals;
88
82
  } catch (error) {
89
83
  // Editing a file we cannot validate is the one way this writer could
90
84
  // quietly make things worse, so refuse rather than open.
@@ -142,34 +136,6 @@ export function registerSettingsCommand(
142
136
  }
143
137
  };
144
138
 
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
139
  /**
174
140
  * Closes the settings dialog. Assigned when `ctx.ui.custom` builds the
175
141
  * component, so it is only ever called from inside the dialog it closes.
@@ -213,13 +179,6 @@ export function registerSettingsCommand(
213
179
  selectTheme: getSelectListTheme(),
214
180
  requestRender: () => tui.requestRender(),
215
181
  };
216
- const standingHost: StandingApprovalsSubmenuHost = {
217
- standingApprovals: standingApprovalSummaries,
218
- revokeStandingApproval: revokeApproval,
219
- settingsTheme: getSettingsListTheme(),
220
- selectTheme: getSelectListTheme(),
221
- requestRender: () => tui.requestRender(),
222
- };
223
182
  const list = new SettingsList(
224
183
  buildSettingItems(
225
184
  settings,
@@ -227,13 +186,9 @@ export function registerSettingsCommand(
227
186
  reviewerModel: createModelSubmenu(host),
228
187
  timeout: createTimeoutSubmenu(host),
229
188
  ...(denialLog.enabled ? { recentDenials: createDenialsSubmenu(denialsHost) } : {}),
230
- ...(standingApprovalsConfig.enabled
231
- ? { standingApprovals: createStandingApprovalsSubmenu(standingHost) }
232
- : {}),
233
189
  },
234
190
  undefined,
235
191
  denialLog.enabled ? recentDenials().length : undefined,
236
- standingApprovalsConfig.enabled ? standingApprovalSummaries().length : undefined,
237
192
  ),
238
193
  10,
239
194
  host.settingsTheme,
package/settings-menu.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * Row model and pure edit logic behind `/auto-permissions`.
3
3
  *
4
4
  * Everything that decides *what* a row shows or what an edit means lives here,
5
- * free of pi's runtime, so it is testable without a TUI. index.ts owns only the
6
- * wiring: read config, render a SettingsList, persist through config-writer.
5
+ * free of pi's runtime, so it is testable without a TUI. settings-command.ts owns
6
+ * only the wiring: read config, render a SettingsList, persist through config-writer.
7
7
  */
8
8
  import { homedir } from "node:os";
9
9
  import {
@@ -24,10 +24,9 @@ import {
24
24
  MIN_REVIEWER_TIMEOUT_MS,
25
25
  REASONING_EFFORTS,
26
26
  type ReasoningEffort,
27
+ type ReviewerConfig,
27
28
  type SystemPromptSource,
28
29
  } from "./config.js";
29
- import type { ReviewerBlock } from "./config-writer.js";
30
- import type { StandingApprovalRecord } from "./standing-overrides.js";
31
30
 
32
31
  const ENABLED_ID = "enabled";
33
32
  const REVIEWER_MODEL_ID = "reviewerModel";
@@ -35,7 +34,6 @@ const THINKING_LEVEL_ID = "reasoningEffort";
35
34
  const TIMEOUT_ID = "timeoutMs";
36
35
  const SYSTEM_PROMPT_ID = "systemPrompt";
37
36
  export const RECENT_DENIALS_ID = "recentDenials";
38
- export const STANDING_APPROVALS_ID = "standingApprovals";
39
37
 
40
38
  export const ON = "on";
41
39
  export const OFF = "off";
@@ -48,7 +46,7 @@ const NO_REVIEWER_MESSAGE = "Select a reviewer model first";
48
46
  /** The slice of the config the menu edits, plus the read-only prompt source. */
49
47
  export interface ReviewerSettings {
50
48
  enabled: boolean;
51
- reviewer?: ReviewerBlock;
49
+ reviewer?: ReviewerConfig;
52
50
  systemPromptSource: SystemPromptSource;
53
51
  }
54
52
 
@@ -141,7 +139,6 @@ export interface SettingSubmenus {
141
139
  reviewerModel?: SettingItem["submenu"];
142
140
  timeout?: SettingItem["submenu"];
143
141
  recentDenials?: SettingItem["submenu"];
144
- standingApprovals?: SettingItem["submenu"];
145
142
  }
146
143
 
147
144
  /** Build the `/auto-permissions` rows for a settings snapshot. */
@@ -150,13 +147,12 @@ export function buildSettingItems(
150
147
  submenus: SettingSubmenus = {},
151
148
  home: string = homedir(),
152
149
  denialCount?: number,
153
- standingApprovalCount?: number,
154
150
  ): SettingItem[] {
155
151
  return [
156
152
  {
157
153
  id: ENABLED_ID,
158
154
  label: "Enabled",
159
- description: "Review guarded commands. Off lets every command run without guardian review.",
155
+ description: "Review guarded commands. Off disables all gating, including deny rules.",
160
156
  currentValue: toggleValue(settings.enabled),
161
157
  values: TOGGLE_VALUES,
162
158
  },
@@ -196,15 +192,6 @@ export function buildSettingItems(
196
192
  submenu: submenus.recentDenials,
197
193
  }]
198
194
  : []),
199
- ...(submenus.standingApprovals
200
- ? [{
201
- id: STANDING_APPROVALS_ID,
202
- label: "Standing approvals",
203
- description: "Comparable-command approvals shared across projects. Select one to revoke it.",
204
- currentValue: standingApprovalCount === undefined ? "" : String(standingApprovalCount),
205
- submenu: submenus.standingApprovals,
206
- }]
207
- : []),
208
195
  ];
209
196
  }
210
197
 
@@ -568,120 +555,6 @@ export function createDenialsSubmenu(host: DenialsSubmenuHost): NonNullable<Sett
568
555
  return (_currentValue, done) => new DenialsSubmenu(host, done);
569
556
  }
570
557
 
571
- export interface StandingApprovalSummary {
572
- id: string;
573
- record: StandingApprovalRecord;
574
- }
575
-
576
- export interface StandingApprovalsSubmenuHost {
577
- standingApprovals(): StandingApprovalSummary[];
578
- revokeStandingApproval(approval: StandingApprovalSummary): void;
579
- settingsTheme: SettingsListTheme;
580
- selectTheme: SelectListTheme;
581
- requestRender(): void;
582
- }
583
-
584
- export function standingApprovalItem(approval: StandingApprovalSummary): SelectItem {
585
- const command = approval.record.command.length > COMMAND_LABEL_MAX
586
- ? `${approval.record.command.slice(0, COMMAND_LABEL_MAX - 1)}…`
587
- : approval.record.command;
588
- return {
589
- value: approval.id,
590
- label: command,
591
- description: `${approval.record.gate.label} · granted ${approval.record.ts.slice(0, 10)} · ${approval.record.project}`,
592
- };
593
- }
594
-
595
- class StandingApprovalsSubmenu implements Component {
596
- private readonly approvals: StandingApprovalSummary[];
597
- private list: SelectList;
598
- private selected?: StandingApprovalSummary;
599
-
600
- constructor(
601
- private readonly host: StandingApprovalsSubmenuHost,
602
- private readonly done: (value?: string) => void,
603
- ) {
604
- this.approvals = host.standingApprovals();
605
- this.list = this.buildApprovalList();
606
- }
607
-
608
- private buildApprovalList(): SelectList {
609
- const items = this.approvals.length
610
- ? this.approvals.map(standingApprovalItem)
611
- : [{ value: "", label: "(no standing approvals)", description: "Comparable-command approvals will appear here." }];
612
- const list = new SelectList(items, 10, this.host.selectTheme);
613
- list.onCancel = () => this.done(undefined);
614
- list.onSelect = (item) => {
615
- const approval = this.approvals.find((candidate) => candidate.id === item.value);
616
- if (!approval) {
617
- this.done(undefined);
618
- return;
619
- }
620
- this.selected = approval;
621
- this.list = this.buildConfirmList();
622
- this.host.requestRender();
623
- };
624
- return list;
625
- }
626
-
627
- private buildConfirmList(): SelectList {
628
- const list = new SelectList(
629
- [
630
- {
631
- value: "revoke",
632
- label: "Revoke",
633
- description: "Removes this approval from the user-scoped standing ledger.",
634
- },
635
- { value: "back", label: "Back" },
636
- ],
637
- 10,
638
- this.host.selectTheme,
639
- );
640
- const back = () => {
641
- this.selected = undefined;
642
- this.list = this.buildApprovalList();
643
- this.host.requestRender();
644
- };
645
- list.onCancel = back;
646
- list.onSelect = (item) => {
647
- if (item.value !== "revoke" || !this.selected) {
648
- back();
649
- return;
650
- }
651
- this.host.revokeStandingApproval(this.selected);
652
- this.done(undefined);
653
- };
654
- return list;
655
- }
656
-
657
- invalidate(): void {
658
- this.list.invalidate();
659
- }
660
-
661
- render(width: number): string[] {
662
- const title = this.selected
663
- ? ` Revoke standing approval? ${truncateToWidth(this.selected.record.command, Math.max(1, width - 30))}`
664
- : " Standing approvals";
665
- return [
666
- truncateToWidth(this.host.settingsTheme.hint(title), width),
667
- "",
668
- ...this.list.render(width),
669
- "",
670
- truncateToWidth(this.host.settingsTheme.hint(" Enter to select · Esc to go back"), width),
671
- ];
672
- }
673
-
674
- handleInput(data: string): void {
675
- this.list.handleInput(data);
676
- }
677
- }
678
-
679
- export function createStandingApprovalsSubmenu(
680
- host: StandingApprovalsSubmenuHost,
681
- ): NonNullable<SettingItem["submenu"]> {
682
- return (_currentValue, done) => new StandingApprovalsSubmenu(host, done);
683
- }
684
-
685
558
  export function createTimeoutSubmenu(host: SubmenuHost): NonNullable<SettingItem["submenu"]> {
686
559
  return (currentValue, done) =>
687
560
  new PromptComponent(
package/usage-log.ts CHANGED
@@ -1,9 +1,5 @@
1
- import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync } from "node:fs";
2
1
  import { randomUUID } from "node:crypto";
3
- import { dirname } from "node:path";
4
-
5
- /** Rotate once the sidecar grows past this size; one previous generation is kept. */
6
- export const USAGE_LOG_ROTATE_BYTES = 16 * 1024 * 1024;
2
+ import { appendJsonlRecord, SIDECAR_ROTATE_BYTES } from "./jsonl-sidecar.js";
7
3
 
8
4
  interface UsageLogTotals {
9
5
  input: number;
@@ -55,14 +51,13 @@ export function buildUsageLogRecord(
55
51
  model: string,
56
52
  usage: unknown,
57
53
  label = "guardian",
58
- source = "auto-permissions",
59
54
  subagent = false,
60
55
  ): UsageLogRecord {
61
56
  return {
62
57
  v: 1,
63
58
  id: randomUUID(),
64
59
  ts: new Date().toISOString(),
65
- source,
60
+ source: "auto-permissions",
66
61
  label,
67
62
  provider,
68
63
  model,
@@ -71,18 +66,6 @@ export function buildUsageLogRecord(
71
66
  };
72
67
  }
73
68
 
74
- function rotateIfLarge(path: string): void {
75
- try {
76
- if (statSync(path).size < USAGE_LOG_ROTATE_BYTES) return;
77
- renameSync(path, `${path}.1`);
78
- } catch {
79
- // A missing or unrotatable sidecar simply keeps appending.
80
- }
81
- }
82
-
83
69
  export function appendUsageRecord(path: string, record: UsageLogRecord): void {
84
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
85
- rotateIfLarge(path);
86
- appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
87
- chmodSync(path, 0o600);
70
+ appendJsonlRecord(path, record, SIDECAR_ROTATE_BYTES);
88
71
  }
package/widget-status.ts CHANGED
@@ -18,8 +18,6 @@ interface ReviewStatusFrame {
18
18
  glyph: string;
19
19
  label: string;
20
20
  tone: ReviewStatusTone;
21
- /** True once no further frames are needed (state is static). */
22
- done: boolean;
23
21
  }
24
22
 
25
23
  export const WAITING_FRAMES = ["✶", "✸", "✻", "✽"] as const;
@@ -41,23 +39,21 @@ export function reviewStatusFrame(
41
39
  glyph: "⋯",
42
40
  label: "queued behind another review",
43
41
  tone: "muted",
44
- done: true,
45
42
  };
46
43
  case "waiting":
47
44
  return {
48
45
  glyph: WAITING_FRAMES[index % WAITING_FRAMES.length],
49
46
  label: `waiting for ${reviewer}`,
50
47
  tone: "warning",
51
- done: false,
52
48
  };
53
49
  case "approved":
54
- return { glyph: "✓", label: "approved", tone: "success", done: true };
50
+ return { glyph: "✓", label: "approved", tone: "success" };
55
51
  case "revise":
56
- return { glyph: "↻", label: "revision requested", tone: "warning", done: true };
52
+ return { glyph: "↻", label: "revision requested", tone: "warning" };
57
53
  case "ask_user":
58
- return { glyph: "?", label: "waiting for your approval", tone: "accent", done: true };
54
+ return { glyph: "?", label: "waiting for your approval", tone: "accent" };
59
55
  case "blocked":
60
- return { glyph: "✗", label: "blocked", tone: "error", done: true };
56
+ return { glyph: "✗", label: "blocked", tone: "error" };
61
57
  }
62
58
  }
63
59
 
@@ -74,15 +70,6 @@ export interface ReviewLinePalette {
74
70
  error: (text: string) => string;
75
71
  }
76
72
 
77
- export const PLAIN_PALETTE: ReviewLinePalette = {
78
- header: (text) => text,
79
- muted: (text) => text,
80
- warning: (text) => text,
81
- success: (text) => text,
82
- accent: (text) => text,
83
- error: (text) => text,
84
- };
85
-
86
73
  /**
87
74
  * Render the widget content: one status line, plus a dim detail line only
88
75
  * when a guardian reason is present. The command is intentionally omitted —