@gotgenes/pi-permission-system 10.7.1 → 10.7.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [10.7.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.7.1...pi-permission-system-v10.7.2) (2026-06-10)
9
+
10
+
11
+ ### Miscellaneous Chores
12
+
13
+ * **deps:** bump tooling dependencies to latest minor/patch ([8b9105d](https://github.com/gotgenes/pi-packages/commit/8b9105d4011816fe8085dfed3a3b9d7bc9918c56))
14
+
8
15
  ## [10.7.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.7.0...pi-permission-system-v10.7.1) (2026-06-09)
9
16
 
10
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "10.7.1",
3
+ "version": "10.7.2",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -60,17 +60,17 @@
60
60
  "@earendil-works/pi-tui": ">=0.75.0"
61
61
  },
62
62
  "devDependencies": {
63
- "@biomejs/biome": "^2.4.14",
63
+ "@biomejs/biome": "^2.4.16",
64
64
  "@earendil-works/pi-coding-agent": "0.75.4",
65
65
  "@earendil-works/pi-tui": "0.75.4",
66
66
  "@types/node": "^22.15.3",
67
- "rumdl": "^0.1.93",
67
+ "rumdl": "^0.2.10",
68
68
  "typescript": "^6.0.3",
69
- "vitest": "^4.1.5"
69
+ "vitest": "^4.1.8"
70
70
  },
71
71
  "dependencies": {
72
72
  "tree-sitter-bash": "^0.25.1",
73
- "web-tree-sitter": "^0.26.8"
73
+ "web-tree-sitter": "^0.26.9"
74
74
  },
75
75
  "scripts": {
76
76
  "check": "tsc --noEmit",
@@ -3,6 +3,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { PermissionResolver } from "#src/permission-resolver";
4
4
  import type { PermissionSession } from "#src/permission-session";
5
5
  import type { ServiceLifecycle } from "#src/service-lifecycle";
6
+ import type { SessionLogger } from "#src/session-logger";
6
7
  import { PERMISSION_SYSTEM_STATUS_KEY } from "#src/status";
7
8
 
8
9
  /** Minimal subset of SessionStartEvent used by this handler. */
@@ -24,12 +25,14 @@ interface ResourcesDiscoverPayload {
24
25
  * - `serviceLifecycle` — owns the process-global service publication;
25
26
  * `activate` publishes (skipped for registered subagent children) and emits
26
27
  * the ready event; `teardown` unsubscribes all session listeners and unpublishes
28
+ * - `logger` — injected directly; replaces the former `session.logger` reach-through
27
29
  */
28
30
  export class SessionLifecycleHandler {
29
31
  constructor(
30
32
  private readonly session: PermissionSession,
31
33
  private readonly resolver: PermissionResolver,
32
34
  private readonly serviceLifecycle: ServiceLifecycle,
35
+ private readonly logger: SessionLogger,
33
36
  ) {}
34
37
 
35
38
  handleSessionStart(
@@ -43,11 +46,11 @@ export class SessionLifecycleHandler {
43
46
  const agentName = this.session.resolveAgentName(ctx);
44
47
  const policyIssues = this.resolver.getConfigIssues(agentName ?? undefined);
45
48
  for (const issue of policyIssues) {
46
- this.session.logger.warn(issue);
49
+ this.logger.warn(issue);
47
50
  }
48
51
 
49
52
  if (event.reason === "reload") {
50
- this.session.logger.debug("lifecycle.reload", {
53
+ this.logger.debug("lifecycle.reload", {
51
54
  triggeredBy: "session_start",
52
55
  reason: event.reason,
53
56
  cwd: ctx.cwd,
@@ -68,7 +71,7 @@ export class SessionLifecycleHandler {
68
71
  }
69
72
 
70
73
  this.session.reload();
71
- this.session.logger.debug("lifecycle.reload", {
74
+ this.logger.debug("lifecycle.reload", {
72
75
  triggeredBy: "resources_discover",
73
76
  reason: event.reason,
74
77
  cwd: this.session.getRuntimeContext()?.cwd ?? null,
package/src/index.ts CHANGED
@@ -28,7 +28,7 @@ import { PermissionSession } from "./permission-session";
28
28
  import { LocalPermissionsService } from "./permissions-service";
29
29
  import { PromptingGateway } from "./prompting-gateway";
30
30
  import { PermissionServiceLifecycle } from "./service-lifecycle";
31
- import { createSessionLogger } from "./session-logger";
31
+ import { PermissionSessionLogger } from "./session-logger";
32
32
  import { SessionRules } from "./session-rules";
33
33
  import { subscribeSubagentLifecycle } from "./subagent-lifecycle-events";
34
34
  import { getSubagentSessionRegistry } from "./subagent-registry";
@@ -43,22 +43,19 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
43
43
  const formatterRegistry = new ToolInputFormatterRegistry();
44
44
  registerBuiltinToolInputFormatters(formatterRegistry);
45
45
 
46
- // Forward reference: configStore is declared before the logger so the
47
- // logger's getConfig thunk can close over the variable; assigned immediately
48
- // after. Typed via cast so the closure compiles without assertions.
49
- // The same null-at-init pattern used in the former createExtensionRuntime.
50
- let configStore = null as unknown as ConfigStore;
46
+ // Both `configStore` and `session` are forward-declared so the logger's
47
+ // lazy thunks can close over them without a cast or null-init holder.
48
+ // TypeScript exempts closure captures from definite-assignment analysis;
49
+ // all synchronous reads occur after the assignments below.
50
+ // eslint-disable-next-line prefer-const -- forward-declared let; `const` requires an initializer
51
+ let configStore: ConfigStore;
52
+ // eslint-disable-next-line prefer-const -- forward-declared let; `const` requires an initializer
53
+ let session: PermissionSession;
51
54
 
52
- // sessionNotify is a mutable holder so the logger's notify closure can
53
- // reach the UI once PermissionSession is constructed. Starts as null;
54
- // notify is a best-effort sink (no-op at factory-init when there is no UI).
55
- let sessionNotify: PermissionSession | null = null;
56
-
57
- const logger = createSessionLogger({
55
+ const logger = new PermissionSessionLogger({
58
56
  globalLogsDir: paths.globalLogsDir,
59
57
  getConfig: () => configStore.current(),
60
- notify: (message) =>
61
- sessionNotify?.getRuntimeContext()?.ui.notify(message, "warning"),
58
+ notify: (message) => session.notify(message),
62
59
  });
63
60
 
64
61
  configStore = new ConfigStore({
@@ -85,8 +82,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
85
82
  forwarder,
86
83
  });
87
84
 
88
- configStore.refresh();
89
-
90
85
  const gateway = new PromptingGateway({
91
86
  config: configStore,
92
87
  subagentSessionsDir: paths.subagentSessionsDir,
@@ -94,9 +89,8 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
94
89
  prompter,
95
90
  });
96
91
 
97
- const session = new PermissionSession(
92
+ session = new PermissionSession(
98
93
  paths,
99
- logger,
100
94
  new ForwardingManager(
101
95
  paths.subagentSessionsDir,
102
96
  forwarder,
@@ -108,8 +102,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
108
102
  gateway,
109
103
  );
110
104
 
111
- // Connect the notify sink now that session is available.
112
- sessionNotify = session;
105
+ // refresh() must run after `session` is assigned: a debug-write IO failure
106
+ // triggers the logger's notify sink — `session.notify(m)` — which no-ops
107
+ // on the null context but requires `session` to be bound.
108
+ configStore.refresh();
113
109
 
114
110
  const configPath = getGlobalConfigPath(agentDir);
115
111
  registerPermissionSystemCommand(pi, {
@@ -163,10 +159,11 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
163
159
  session,
164
160
  resolver,
165
161
  serviceLifecycle,
162
+ logger,
166
163
  );
167
164
  const agentPrep = new AgentPrepHandler(session, resolver, toolRegistry);
168
165
 
169
- const reporter = new GateDecisionReporter(session.logger, pi.events);
166
+ const reporter = new GateDecisionReporter(logger, pi.events);
170
167
  const gateRunner = new GateRunner(resolver, sessionRules, gateway, reporter);
171
168
  const toolCallGatePipeline = new ToolCallGatePipeline(
172
169
  resolver,
@@ -13,7 +13,6 @@ import type { ToolCallGateInputs } from "./handlers/gates/tool-call-gate-pipelin
13
13
  import type { ScopedPermissionManager } from "./permission-manager";
14
14
  import type { PromptingGatewayLifecycle } from "./prompting-gateway";
15
15
 
16
- import type { SessionLogger } from "./session-logger";
17
16
  import type { SessionRules } from "./session-rules";
18
17
  import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
19
18
  import {
@@ -31,7 +30,6 @@ import {
31
30
  *
32
31
  * Constructor deps:
33
32
  * - `ExtensionPaths` — immutable path constants
34
- * - `SessionLogger` — debug + review + warn
35
33
  * - `ForwardingController` — polling lifecycle
36
34
  * - `SessionConfigStore` — owns extension config; provides refresh, log, read
37
35
  * - `PromptingGatewayLifecycle` — prompting lifecycle forwarded via activate/deactivate
@@ -45,7 +43,6 @@ export class PermissionSession implements ToolCallGateInputs {
45
43
 
46
44
  constructor(
47
45
  private readonly paths: ExtensionPaths,
48
- readonly logger: SessionLogger,
49
46
  private readonly forwarding: ForwardingController,
50
47
  private readonly permissionManager: ScopedPermissionManager,
51
48
  private readonly sessionRules: SessionRules,
@@ -74,6 +71,13 @@ export class PermissionSession implements ToolCallGateInputs {
74
71
  return this.context;
75
72
  }
76
73
 
74
+ // ── UI notifications ────────────────────────────────────────────────────
75
+
76
+ /** Surface a warning message to the user via the active UI context, if any. */
77
+ notify(message: string): void {
78
+ this.context?.ui.notify(message, "warning");
79
+ }
80
+
77
81
  // ── Session lifecycle ────────────────────────────────────────────────────
78
82
 
79
83
  /**
@@ -4,7 +4,10 @@ import {
4
4
  ensurePermissionSystemLogsDirectory,
5
5
  type PermissionSystemExtensionConfig,
6
6
  } from "./extension-config";
7
- import { createPermissionSystemLogger } from "./logging";
7
+ import {
8
+ createPermissionSystemLogger,
9
+ type PermissionSystemLogger,
10
+ } from "./logging";
8
11
 
9
12
  /**
10
13
  * Narrowest logging seam — consumers that only write review-log entries.
@@ -44,37 +47,45 @@ export interface SessionLoggerDeps {
44
47
  }
45
48
 
46
49
  /**
47
- * Create a SessionLogger from narrow dependencies.
50
+ * Concrete `SessionLogger` implementation.
48
51
  *
49
- * Composes the JSONL log writer, owns the IO-failure warning dedup Set,
50
- * and routes both IO-failure warnings and explicit warn() calls through
51
- * the injected notify sink. No ExtensionRuntime reference required.
52
+ * Composes the JSONL log writer, privately owns the IO-failure warning
53
+ * dedup Set, and routes both IO-failure warnings and explicit warn() calls
54
+ * through the injected notify sink. No ExtensionRuntime reference required.
52
55
  */
53
- export function createSessionLogger(deps: SessionLoggerDeps): SessionLogger {
54
- const writer = createPermissionSystemLogger({
55
- getConfig: deps.getConfig,
56
- debugLogPath: join(deps.globalLogsDir, DEBUG_LOG_FILENAME),
57
- reviewLogPath: join(deps.globalLogsDir, REVIEW_LOG_FILENAME),
58
- ensureLogsDirectory: () =>
59
- ensurePermissionSystemLogsDirectory(deps.globalLogsDir),
60
- });
56
+ export class PermissionSessionLogger implements SessionLogger {
57
+ private readonly writer: PermissionSystemLogger;
58
+ private readonly reported = new Set<string>();
59
+ private readonly notify: (message: string) => void;
60
+
61
+ constructor(deps: SessionLoggerDeps) {
62
+ this.writer = createPermissionSystemLogger({
63
+ getConfig: deps.getConfig,
64
+ debugLogPath: join(deps.globalLogsDir, DEBUG_LOG_FILENAME),
65
+ reviewLogPath: join(deps.globalLogsDir, REVIEW_LOG_FILENAME),
66
+ ensureLogsDirectory: () =>
67
+ ensurePermissionSystemLogsDirectory(deps.globalLogsDir),
68
+ });
69
+ this.notify = deps.notify;
70
+ }
71
+
72
+ debug(event: string, details?: Record<string, unknown>): void {
73
+ const warning = this.writer.debug(event, details);
74
+ if (warning) this.reportOnce(warning);
75
+ }
76
+
77
+ review(event: string, details?: Record<string, unknown>): void {
78
+ const warning = this.writer.review(event, details);
79
+ if (warning) this.reportOnce(warning);
80
+ }
61
81
 
62
- const reported = new Set<string>();
63
- const reportOnce = (warning: string): void => {
64
- if (reported.has(warning)) return;
65
- reported.add(warning);
66
- deps.notify(warning);
67
- };
82
+ warn(message: string): void {
83
+ this.notify(message);
84
+ }
68
85
 
69
- return {
70
- debug: (event, details) => {
71
- const warning = writer.debug(event, details);
72
- if (warning) reportOnce(warning);
73
- },
74
- review: (event, details) => {
75
- const warning = writer.review(event, details);
76
- if (warning) reportOnce(warning);
77
- },
78
- warn: (message) => deps.notify(message),
79
- };
86
+ private reportOnce(warning: string): void {
87
+ if (this.reported.has(warning)) return;
88
+ this.reported.add(warning);
89
+ this.notify(warning);
90
+ }
80
91
  }
@@ -191,14 +191,13 @@ describe("external_directory policy state — allow", () => {
191
191
  });
192
192
 
193
193
  it("does not write a block review-log entry when external_directory is allow", async () => {
194
- const { handler, session } = makeHandler({
194
+ const { handler, logger } = makeHandler({
195
195
  session: { checkPermission: makeExtDirCheck("allow") },
196
196
  tools: ALL_TOOLS,
197
197
  });
198
198
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
199
199
  await handler.handleToolCall(event, makeCtx());
200
- const reviewCalls = (session.logger.review as ReturnType<typeof vi.fn>).mock
201
- .calls;
200
+ const reviewCalls = (logger.review as ReturnType<typeof vi.fn>).mock.calls;
202
201
  const blockEntries = reviewCalls.filter(
203
202
  ([eventName]: string[]) => eventName === "permission_request.blocked",
204
203
  );
@@ -301,14 +300,13 @@ describe("external_directory policy state — deny", () => {
301
300
  });
302
301
 
303
302
  it("writes review-log entry with resolution policy_denied", async () => {
304
- const { handler, session } = makeHandler({
303
+ const { handler, logger } = makeHandler({
305
304
  session: { checkPermission: makeExtDirCheck("deny") },
306
305
  tools: ALL_TOOLS,
307
306
  });
308
307
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
309
308
  await handler.handleToolCall(event, makeCtx());
310
- const reviewCalls = (session.logger.review as ReturnType<typeof vi.fn>).mock
311
- .calls;
309
+ const reviewCalls = (logger.review as ReturnType<typeof vi.fn>).mock.calls;
312
310
  const blockEntries = reviewCalls.filter(
313
311
  ([eventName]: string[]) => eventName === "permission_request.blocked",
314
312
  );
@@ -458,7 +456,7 @@ describe("external_directory policy state — ask", () => {
458
456
  });
459
457
 
460
458
  it("writes review-log entry with confirmation_unavailable when no UI", async () => {
461
- const { handler, session } = makeHandler({
459
+ const { handler, logger } = makeHandler({
462
460
  session: { checkPermission: makeExtDirCheck("ask") },
463
461
  prompter: {
464
462
  canConfirm: vi.fn().mockReturnValue(false),
@@ -468,8 +466,7 @@ describe("external_directory policy state — ask", () => {
468
466
  });
469
467
  const event = makeToolCallEvent("read", { input: { path: EXTERNAL_PATH } });
470
468
  await handler.handleToolCall(event, makeCtx({ hasUI: false }));
471
- const reviewCalls = (session.logger.review as ReturnType<typeof vi.fn>).mock
472
- .calls;
469
+ const reviewCalls = (logger.review as ReturnType<typeof vi.fn>).mock.calls;
473
470
  const blockEntries = reviewCalls.filter(
474
471
  ([eventName]: string[]) => eventName === "permission_request.blocked",
475
472
  );
@@ -5,6 +5,7 @@ import type { ServiceLifecycle } from "#src/service-lifecycle";
5
5
 
6
6
  import { makeCtx } from "#test/helpers/handler-fixtures";
7
7
  import {
8
+ makeLogger,
8
9
  makeRealResolver,
9
10
  makeRealSession,
10
11
  } from "#test/helpers/session-fixtures";
@@ -19,14 +20,8 @@ vi.mock("../../src/status", () => ({
19
20
  // ── helpers ────────────────────────────────────────────────────────────────
20
21
 
21
22
  function makeSetup(opts?: { configIssues?: string[] }) {
22
- const {
23
- session,
24
- permissionManager,
25
- sessionRules,
26
- logger,
27
- forwarding,
28
- configStore,
29
- } = makeRealSession();
23
+ const { session, permissionManager, sessionRules, forwarding, configStore } =
24
+ makeRealSession();
30
25
  const { resolver } = makeRealResolver(permissionManager, sessionRules);
31
26
  if (opts?.configIssues) {
32
27
  vi.mocked(permissionManager.getConfigIssues).mockReturnValue(
@@ -37,10 +32,14 @@ function makeSetup(opts?: { configIssues?: string[] }) {
37
32
  activate: vi.fn<ServiceLifecycle["activate"]>(),
38
33
  teardown: vi.fn<ServiceLifecycle["teardown"]>(),
39
34
  };
35
+ // Use a session-independent logger so assertions verify direct injection,
36
+ // not reach-through to session.logger.
37
+ const logger = makeLogger();
40
38
  const handler = new SessionLifecycleHandler(
41
39
  session,
42
40
  resolver,
43
41
  serviceLifecycle,
42
+ logger,
44
43
  );
45
44
  return {
46
45
  handler,
@@ -25,7 +25,6 @@ import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
25
25
  import type { PermissionDecisionEvent } from "#src/permission-events";
26
26
  import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
27
27
  import type { Rule } from "#src/rule";
28
- import type { SessionLogger } from "#src/session-logger";
29
28
  import { SessionRules } from "#src/session-rules";
30
29
  import type { ToolRegistry } from "#src/tool-registry";
31
30
  import type { PermissionCheckResult, PermissionState } from "#src/types";
@@ -49,8 +48,6 @@ import {
49
48
  */
50
49
  export type MockGateHandlerSession = ToolCallGateInputs &
51
50
  SkillInputGateInputs & {
52
- /** Logger shape expected by GateDecisionReporter. */
53
- logger: SessionLogger;
54
51
  /** 4-arg form so surface-check mocks can receive optional rules. */
55
52
  checkPermission(
56
53
  surface: string,
@@ -296,6 +293,7 @@ export function makeHandler(overrides?: {
296
293
  handler,
297
294
  events,
298
295
  session,
296
+ logger,
299
297
  toolRegistry,
300
298
  prompter,
301
299
  recorder,
@@ -150,7 +150,6 @@ export function makeRealSession(overrides?: {
150
150
  const gateway = overrides?.gateway ?? makeGateway();
151
151
  const session = new PermissionSession(
152
152
  paths,
153
- logger,
154
153
  forwarding,
155
154
  permissionManager,
156
155
  sessionRules,
@@ -388,4 +388,35 @@ describe("PermissionSession", () => {
388
388
  expect(session.getRuntimeContext()).toBeNull();
389
389
  });
390
390
  });
391
+
392
+ describe("notify", () => {
393
+ it("forwards the message to ctx.ui.notify with 'warning' severity after activation", () => {
394
+ const { session } = createSession();
395
+ const ctx = makeCtx();
396
+ session.activate(ctx);
397
+
398
+ session.notify("something went wrong");
399
+
400
+ expect(ctx.ui.notify).toHaveBeenCalledOnce();
401
+ expect(ctx.ui.notify).toHaveBeenCalledWith(
402
+ "something went wrong",
403
+ "warning",
404
+ );
405
+ });
406
+
407
+ it("is a no-op and does not throw before activation", () => {
408
+ const { session } = createSession();
409
+
410
+ expect(() => session.notify("msg")).not.toThrow();
411
+ });
412
+
413
+ it("is a no-op and does not throw after deactivation", () => {
414
+ const { session } = createSession();
415
+ const ctx = makeCtx();
416
+ session.activate(ctx);
417
+ session.deactivate();
418
+
419
+ expect(() => session.notify("msg")).not.toThrow();
420
+ });
421
+ });
391
422
  });
@@ -8,7 +8,7 @@ import {
8
8
  type PermissionSystemExtensionConfig,
9
9
  } from "#src/extension-config";
10
10
  import type { SessionLoggerDeps } from "#src/session-logger";
11
- import { createSessionLogger } from "#src/session-logger";
11
+ import { PermissionSessionLogger } from "#src/session-logger";
12
12
 
13
13
  // ── helpers ────────────────────────────────────────────────────────────────
14
14
 
@@ -42,9 +42,9 @@ function makeBlockedLogsDir(): string {
42
42
  return join(barrier, "logs");
43
43
  }
44
44
 
45
- // ── createSessionLogger ────────────────────────────────────────────────────
45
+ // ── PermissionSessionLogger ────────────────────────────────────────────────────
46
46
 
47
- describe("createSessionLogger", () => {
47
+ describe("PermissionSessionLogger", () => {
48
48
  // ── debug ────────────────────────────────────────────────────────────────
49
49
 
50
50
  describe("debug", () => {
@@ -52,7 +52,7 @@ describe("createSessionLogger", () => {
52
52
  const deps = makeDeps({
53
53
  getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
54
54
  });
55
- const logger = createSessionLogger(deps);
55
+ const logger = new PermissionSessionLogger(deps);
56
56
 
57
57
  logger.debug("test.event", { key: "value" });
58
58
 
@@ -63,7 +63,7 @@ describe("createSessionLogger", () => {
63
63
  it("does not write to the debug log when debugLog is false", () => {
64
64
  // DEFAULT_EXTENSION_CONFIG.debugLog === false
65
65
  const deps = makeDeps();
66
- const logger = createSessionLogger(deps);
66
+ const logger = new PermissionSessionLogger(deps);
67
67
 
68
68
  logger.debug("test.event");
69
69
 
@@ -76,7 +76,7 @@ describe("createSessionLogger", () => {
76
76
  const deps = makeDeps({
77
77
  getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog }),
78
78
  });
79
- const logger = createSessionLogger(deps);
79
+ const logger = new PermissionSessionLogger(deps);
80
80
  debugLog = false;
81
81
 
82
82
  logger.debug("test.event");
@@ -91,7 +91,7 @@ describe("createSessionLogger", () => {
91
91
  it("writes a JSONL line to the review log file when permissionReviewLog is true", () => {
92
92
  // DEFAULT_EXTENSION_CONFIG.permissionReviewLog === true
93
93
  const deps = makeDeps();
94
- const logger = createSessionLogger(deps);
94
+ const logger = new PermissionSessionLogger(deps);
95
95
 
96
96
  logger.review("permission.granted", { agentName: "coder" });
97
97
 
@@ -106,7 +106,7 @@ describe("createSessionLogger", () => {
106
106
  permissionReviewLog: false,
107
107
  }),
108
108
  });
109
- const logger = createSessionLogger(deps);
109
+ const logger = new PermissionSessionLogger(deps);
110
110
 
111
111
  logger.review("permission.granted");
112
112
 
@@ -123,7 +123,7 @@ describe("createSessionLogger", () => {
123
123
  globalLogsDir: makeBlockedLogsDir(),
124
124
  getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
125
125
  });
126
- const logger = createSessionLogger(deps);
126
+ const logger = new PermissionSessionLogger(deps);
127
127
 
128
128
  logger.debug("test.event");
129
129
 
@@ -138,7 +138,7 @@ describe("createSessionLogger", () => {
138
138
  globalLogsDir: makeBlockedLogsDir(),
139
139
  getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
140
140
  });
141
- const logger = createSessionLogger(deps);
141
+ const logger = new PermissionSessionLogger(deps);
142
142
 
143
143
  logger.debug("event.one");
144
144
  logger.debug("event.two");
@@ -155,7 +155,7 @@ describe("createSessionLogger", () => {
155
155
  permissionReviewLog: true,
156
156
  }),
157
157
  });
158
- const logger = createSessionLogger(deps);
158
+ const logger = new PermissionSessionLogger(deps);
159
159
 
160
160
  logger.debug("event.one"); // emits warning
161
161
  logger.review("event.two"); // same error message → suppressed
@@ -169,7 +169,7 @@ describe("createSessionLogger", () => {
169
169
  describe("warn", () => {
170
170
  it("calls notify with the message directly", () => {
171
171
  const deps = makeDeps();
172
- const logger = createSessionLogger(deps);
172
+ const logger = new PermissionSessionLogger(deps);
173
173
 
174
174
  logger.warn("Something went wrong");
175
175
 
@@ -178,7 +178,7 @@ describe("createSessionLogger", () => {
178
178
 
179
179
  it("calls notify for every warn — not deduplicated", () => {
180
180
  const deps = makeDeps();
181
- const logger = createSessionLogger(deps);
181
+ const logger = new PermissionSessionLogger(deps);
182
182
 
183
183
  logger.warn("same message");
184
184
  logger.warn("same message");
@@ -192,7 +192,7 @@ describe("createSessionLogger", () => {
192
192
  getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG }),
193
193
  notify: () => {},
194
194
  };
195
- const logger = createSessionLogger(deps);
195
+ const logger = new PermissionSessionLogger(deps);
196
196
 
197
197
  expect(() => logger.warn("test")).not.toThrow();
198
198
  });