Files
my-pi/pi-permission-system/docs/retro/0043-eliminate-module-scope-state.md
T

3.0 KiB

issue, issue_title
issue issue_title
43 Eliminate module-scope mutable state and cached getAgentDir() in src/index.ts

Retro: #43 — Eliminate module-scope mutable state

Final Retrospective (2026-05-03T19:04:00Z)

Session summary

Replaced all module-scope mutable state in src/index.ts (cached getAgentDir() paths, mutable config, logger singletons, setter-injection functions) with an ExtensionRuntime context object created at factory invocation time. src/index.ts went from 466 → 99 lines; src/runtime.ts (318 lines) now holds the runtime interface, factory, and all relocated helpers. The forwarded-permissions IO module was also refactored to accept an explicit logger parameter instead of using a module-scope singleton.

Observations

What went well

  • The 7-step TDD sequence from the plan executed cleanly — each commit was independently valid and the full test suite stayed green throughout. The plan's decision to add runtime to HandlerDeps alongside the old stubs (step 3) before removing the stubs (step 4) allowed both phases to compile and test independently.
  • The createExtensionRuntime({ agentDir: tmpDir }) pattern immediately proved its value: 29 tests in tests/runtime.test.ts exercise the runtime in isolation without any PI_CODING_AGENT_DIR timing hacks.
  • Forwarded-permissions logger threading touched 30+ call sites but landed in a single clean commit with no rework — the mechanical nature of "prepend logger parameter" made it safe to do in bulk.

What caused friction (agent side)

  1. missing-context — Used vi.fn(() => ({})) to mock PermissionManager constructor in tests/runtime.test.ts. Arrow functions are not constructable, so new PermissionManager() threw "() => ({}) is not a constructor". The same pattern caused friction in #42. Impact: 1 failed test run + 1 edit to fix; added friction but no rework beyond the immediate fix. Self-identified.

  2. missing-context — Wrote await import("../src/permission-manager") inside non-async it() callbacks in step 2 tests for createPermissionManagerForCwd. Biome flagged await outside async function. Impact: 1 failed lint + 1 edit to switch to a static top-level import of the already-mocked PermissionManager. Self-identified.

  3. wrong-abstraction — The plan's step 3 ("update test mocks") and step 4 ("update handlers + type") were described as sequential, but TypeScript rejects extra properties on typed object literals, so runtime couldn't be added to test mocks until HandlerDeps declared the field. The solution was to add runtime to HandlerDeps (with old stubs still present) in step 3, making the type change part of the same commit. Impact: minor plan deviation but no rework — the commit sequence remained valid. Self-identified.

What caused friction (user side)

  • Nothing notable. The user provided a clear plan, and the session ran without corrections or redirections.

Changes made

  1. Added class-constructor mocking rule to AGENTS.md § Testing.