Files
my-pi/pi-permission-system/docs/retro/0334-inject-single-permission-manager.md
T

9.0 KiB
Raw Blame History

issue, issue_title
issue issue_title
334 Inject a single PermissionManager into PermissionSession (configure once at session_start)

Retro: #334 — Inject a single PermissionManager into PermissionSession

Stage: Planning (2026-06-04T00:00:00Z)

Session summary

Produced the numbered implementation plan docs/plans/0334-inject-single-permission-manager.md for Phase 4 Step 1. The plan injects one PermissionManager into PermissionSession, adds PermissionManager.configureForCwd(cwd) plus an agentDir constructor option, moves the cwd→paths derivation onto a pure derivePolicyLoaderOptions helper in permission-manager.ts, and deletes the now-dead createPermissionManagerForCwd / derivePiProjectPaths factory functions. Structured as three commits: additive configureForCwd (feat:), the coupled session-injection + index.ts + test change (refactor:), and the factory removal (refactor:).

Observations

  • The package has two separate PermissionManager instances today (runtime.permissionManager vs. the session's). The plan deliberately preserves this split-brain — unifying is Step 4 (#337). Called out in Non-Goals and Risks so the implementer/reviewer does not treat it as a regression.
  • Decided to inject a narrow ScopedPermissionManager interface (five methods) rather than the concrete class, which is what lets permission-session.test.ts drop the as unknown as PermissionManager cast — the issue's stated outcome. getComposedConfigRules / getResolvedPolicyPaths are excluded (ISP) since only the runtime.permissionManager path uses them.
  • The cwd→paths helper had to move out of runtime.ts into permission-manager.ts to avoid an import cycle (runtime.ts imports permission-manager.ts). config-paths.ts is cycle-free, so the helper imports getGlobalConfigPath / getProjectConfigPath from there.
  • Intentional tightening: derivePolicyLoaderOptions sets agentsDir = join(agentDir, "agents") explicitly. Today createPermissionManagerForCwd leaves it unset, so FilePolicyLoader falls back to a hidden getAgentDir() env read. In production agentDir === getAgentDir(), so this is observably identical while removing the env dependency and making the new unit test deterministic.
  • Did not invoke ask_user: the issue's "Proposed change" plus the architecture roadmap resolved every design choice (no genuinely ambiguous breaking-vs-non-breaking or result-shape decision remained).
  • The pre-existing 0334-phase-4-roadmap.md retro is from the roadmap meta-session, not a prior attempt at this implementation; used a distinct slug (0334-inject-single-permission-manager) for both plan and retro.

Stage: Implementation — TDD (2026-06-04T16:50:00Z)

Session summary

Completed all three TDD cycles from the plan. Step 1 (feat:) added PermissionManager.configureForCwd, the agentDir option, derivePolicyLoaderOptions, and the ScopedPermissionManager interface with 5 new filesystem-backed unit tests. Step 2 (refactor:) injected the manager into PermissionSession and updated index.ts; vi.mock("../src/runtime") and as unknown as PermissionManager were removed from permission-session.test.ts. Step 3 (refactor:) deleted createPermissionManagerForCwd and derivePiProjectPaths from runtime.ts and their 8 test blocks from runtime.test.ts. Test count: 1834 → 1831 (3 net: +5 new configureForCwd tests, 8 deleted factory tests).

Observations

  • Pre-completion reviewer returned PASS with two WARNs fixed before committing: (1) unused getGlobalConfigPath import left in test/runtime.test.ts after deleting the factory describe blocks, and (2) derivePolicyLoaderOptions placed above its caller in permission-manager.ts (stepdown rule violation); both were fixed by amending the Step 3 commit.
  • A botched intermediate edit accidentally split PermissionManager into two class declarations while repositioning derivePolicyLoaderOptions. Fixed by removing the spurious early } and re-inserting the helper after the class's real closing brace. The lesson: when moving a helper below its caller in a class file, use two separate focused edits (remove from old location, insert at new location) rather than one large combined replace.
  • The plan's makePermissionManager overrides parameter was dropped entirely in favour of the per-field ?? pattern (testing skill convention); callers that needed custom return values use vi.mocked(pm.method).mockReturnValue(...) after construction instead.
  • The ScopedPermissionManager interface (5 methods) was introduced in Step 1 and consumed in Step 2 with no intermediate dead-code flag from fallow, confirming same-plan cross-step exports are acceptable.

Stage: Final Retrospective (2026-06-05T00:45:38Z)

Session summary

Shipped Phase 4 Step 1 end-to-end across Planning, TDD, and Ship stages in a single session: injected one PermissionManager into PermissionSession, added configureForCwd + the agentDir option, and deleted the createPermissionManagerForCwd / derivePiProjectPaths factories. Released as pi-permission-system-v10.2.0 after CI passed; the issue was closed and the release-please PR (#343) merged. The run was clean overall — the only agent-side rework was a self-inflicted editing slip during a reviewer-flagged cleanup, caught immediately by biome.

Observations

What went well

  • The prior 0334-phase-4-roadmap.md retro's changes paid off: that meta-session flagged premature-convergence and instruction-violation (not reading tests / not loading design-review before planning). This session's Planning stage loaded design-review, read the test files alongside production code, and produced a plan that needed no ask_user round — the exact behavior those retro changes were meant to induce.
  • The ScopedPermissionManager narrow-interface seam delivered the constructibility goal precisely: vi.mock("../src/runtime") and the as unknown as PermissionManager cast both left permission-session.test.ts, and the same-plan cross-step export (interface in Step 1, consumer in Step 2) passed fallow with no intermediate dead-code flag.
  • The pre-completion reviewer (claude-sonnet-4-6) caught two genuine WARNs that would otherwise have shipped — a fresh-context read returning real value on a refactor whose whole point was structural cleanliness.
  • Ship-stage ask_user release-timing gate fired correctly for a multi-issue sequence (Phase 4 is #334#342); the user chose to release Step 1 individually and the release landed cleanly.

What caused friction (agent side)

  • instruction-violation (reviewer-caught) — derivePolicyLoaderOptions was placed above class PermissionManager, violating the stepdown rule that code-design states explicitly ("place it below the function that calls it, not above"). The violation originated in the Planning-stage code sketch (the plan showed the helper above the class) and was followed faithfully during TDD. Impact: one reviewer WARN; the fix was a ~15-line reposition.
  • other (mechanical edit slip) — repositioning derivePolicyLoaderOptions below the class was attempted as one large combined Edit that closed the class early and reopened it as _PermissionManagerMethods, producing a duplicate PermissionManager declaration. Impact: ~5 extra tool calls to recover; caught immediately by the biome noRedeclare / noUnusedPrivateClassMembers autoformat feedback (the feedback loop working as designed), so no rework escaped the session.

What caused friction (user side)

  • None. User involvement was limited to the one strategic decision the workflow is designed to surface (release now vs. batch the sequence); no corrections or redirects were needed.

Diagnostic details

  • Model-performance correlation — one subagent dispatched (pre-completion-reviewer) on anthropic/claude-sonnet-4-6, appropriate for judgment-heavy review; no mismatch. A transient model_change to opencode-go/deepseek-v4-flash appears in the session log with no attributable assistant turn — a selection that did not run; not over-interpreted.
  • Escalation-delay tracking — the class-split slip spanned ~5 tool calls (botched edit → biome error → read → rename → noRedeclare error → read → corrective edits → green), but each step made forward progress against a concrete compiler/linter message rather than looping on the same error; no subagent escalation was warranted.
  • Feedback-loop gap analysis — no gap. pnpm run check ran after every TDD step, the full suite after each step, and pnpm fallow dead-code from the repo root before push; verification was incremental, not end-loaded.

Changes made

  1. Appended this Final Retrospective stage entry to packages/pi-permission-system/docs/retro/0334-inject-single-permission-manager.md. No prompt or AGENTS.md changes were made: the user chose retro-only, since the preventing rules (the code-design stepdown rule) already exist and biome plus the pre-completion reviewer caught both friction points in-session.