---
issue: 108
issue_title: "refactor: extract PolicyLoader from PermissionManager"
---

# Retro: #108 — extract PolicyLoader from PermissionManager

## Final Retrospective (2026-05-07T02:00:00Z)

### Session summary

Extracted all file I/O and mtime caching from `PermissionManager` into a new `PolicyLoader` interface + `FilePolicyLoader` class.
Added 34 new tests including 25 in-memory stub tests that exercise merge and evaluation logic without touching the filesystem.
Released as v5.5.0.

### Observations

#### What went well

- The user's intervention during planning — asking to add test impact analysis (what new tests are enabled, what existing tests become redundant) — significantly improved the plan quality.
  This two-question framework ("what can we now test that we couldn't before?"
  / "what existing tests are now redundant?") is a reusable pattern for extraction refactors.
- The TDD cycle was notably clean for steps 1–3: all tests passed on first attempt.
  The in-memory `PolicyLoader` stub pattern worked exactly as designed, producing 25 tests that are faster, simpler, and more focused than their filesystem-dependent equivalents.
- Backward compatibility was maintained seamlessly — all 1161 pre-existing tests continued to pass after the extraction without any modifications.

#### What caused friction (agent side)

1. `wrong-abstraction` — In TDD step 2, initially used `require()` for lazy-importing `FilePolicyLoader` inside the `PermissionManager` constructor, citing "circular issues."
   There was no circular dependency risk (`policy-loader.ts` does not import from `permission-manager.ts`), and `require()` is inappropriate in an ESM project.
   Self-identified immediately; replaced with a direct static import.
   Impact: no rework commit needed — caught before the file was committed.

2. `missing-context` — In TDD step 4, wrote three tests based on wrong assumptions:
   - **Cache stamp test**: assumed `getCacheStamp()` without an agent name would differ from `getCacheStamp("missing-agent")`, but both produce `"missing"` for the agent slot when no file exists.
     Fixed by creating an actual agent file so the stamps genuinely differ.
   - **YAML frontmatter test**: used inline JSON-in-YAML syntax (`bash: { "git *": allow }`) which the simple YAML parser doesn't handle.
     Fixed by using multi-line YAML.
   - **Config issue test**: assumed an invalid permission value like `"invalid_value"` would trigger a config issue, but `normalizeUnifiedConfig` silently normalizes unknown values.
     Fixed by using malformed JSON that triggers a file-read error.
   Self-identified during test run.
   Impact: one round of test fixes within the same step, no extra commits.

#### What caused friction (user side)

- The user's request to add test impact analysis to the plan was highly valuable but came as a follow-up after the initial plan was committed.
  Integrating this as a standard section in the `/plan-issue` prompt template would avoid the extra round-trip for extraction refactors.

### Changes made

1. `.pi/prompts/plan-issue.md` — added **Test Impact Analysis** as a standard section between Module-Level Changes and TDD Order, covering new tests enabled, redundant tests, and tests that must stay.
