Files
my-pi/pi-permission-system/docs/retro/0325-narrow-permission-gate-handler-roles.md
T

120 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 325
issue_title: "Depend on session role interfaces in PermissionGateHandler, not the concrete PermissionSession class"
---
# Retro: #325 — Depend on session role interfaces in PermissionGateHandler
## Stage: Planning (2026-06-03T04:47:43Z)
### Session summary
Produced the implementation plan for retyping `PermissionGateHandler` against a narrow `GateHandlerSession` role and dropping the `as unknown as PermissionSession` casts.
The issue body (written before [#326]/[#327] landed) lists 12 session members and an open "residual cluster" question; the current code already shrank that residual to four methods (`activate`, `resolveAgentName`, `checkPermission`, `createPermissionRequestId`), so the plan is a small finishing move.
Stepping back per the maintainer's prompt, I expanded the design to inject the pre-built `GateRunner` (not just the `DecisionReporter`), filed three follow-up issues, and updated the Phase 3 roadmap.
### Observations
- The referenced dependencies [#319]/[#322]/[#323] are still **open** in the tracker but their code (`permission-resolver.ts`, `decision-reporter.ts`, `gate-prompter.ts`, `session-approval-recorder.ts`) is merged, and later phases [#326]/[#327] are done — so [#325] is unblocked despite the open labels.
- Decision (confirmed via `ask_user`): inject the whole `GateRunner` rather than only the `DecisionReporter` the roadmap originally named.
This narrows the handler's `session` role to exactly four methods (the three runner roles move to the `index.ts` wiring) and removes the `session.logger` reach-through — the same LoD smell [#322] removed from the runner.
Also drops the `events` constructor param.
- Decision: define a flat four-method `GateHandlerSession` rather than pre-splitting a two-method `SessionContext` base.
A `SessionContext` abstraction gets a second consumer only with [#329]/[#331], so introducing it now would be a speculative export `fallow` could flag.
- The shared `makeSession` in `handler-fixtures.ts` is used **only** by `PermissionGateHandler` tests; `before-agent-start.test.ts` and `lifecycle.test.ts` have their own local `makeSession` and import only `makeCtx`.
So narrowing the shared fixture is safe and does not touch the other handlers.
- Cast-removal wrinkle to watch in implementation: the mocks' `resolve`/`canConfirm`/`promptPermission` delegate to `checkPermission`/`canPrompt`/`prompt` and are currently assigned **after** the `as unknown as` cast.
Without the cast the object literal must satisfy the type at creation; the plan resolves this by defining the delegations inline as closures that read the final `session` object at call time, then spreading `...overrides` last (replacing the `Object.hasOwn` guards).
`external-directory-session-dedup.test.ts` is the canary because it drives stateful session-approval through these delegations.
- Two vestigial mock members (`getToolPermission`, `config`) exist only to satisfy the concrete class and can be dropped once the type is narrowed.
- Broader findings filed as issues (maintainer approved stepping back): [#329] extract a `SkillInputGatePipeline` (the `handleInput` skill-input assembly is still inline, asymmetric with `ToolCallGatePipeline`); [#330] relocate `createPermissionRequestId` off `PermissionSession` (it touches zero session state — maintainer noted it should land on the request-creation collaborator, not a free function); [#331] narrow `AgentPrepHandler` + `SessionLifecycleHandler` the same way.
- Behavior-preserving constraint kept: the skill-input pre-check stays on raw `checkPermission` (no session rules); switching it to `resolve` is a behavior change deferred to [#329].
- Roadmap integration (second pass, on review feedback): the three follow-ups were first parked in an ad-hoc "Phase 3 follow-ups" table, which deviated from the roadmap convention (one issue per numbered step + a node in the Mermaid graph).
Reworked them into proper Steps and graph nodes.
- Resequencing (third pass, on review feedback): [#329] (`SkillInputGatePipeline`) introduces a new collaborator that `index.ts` must construct, so it must land **before** [#320] (the composition-root reframe) — otherwise [#320] cools the `index.ts` hotspot only for [#329] to re-touch it.
Renumbered the Phase 3 tail so reading order matches execution order: Step 12 [#329], Step 13 [#330], Step 14 [#331], Step 15 [#320], Step 16 [#321]; updated the dependency diagram (`S12 --> S15`), the prose, the Tracks table, and the plan's Non-Goals cross-reference.
- Tooling friction: `pi-autoformat` re-pads Mermaid blocks and tables after every `Write`/`Edit`, so batched multi-edit calls against those regions went stale mid-call and failed atomically.
Splitting into smaller targeted edits (and using length-preserving replacements for padded table cells) landed them cleanly.
Worth remembering for any future edit touching the architecture doc's diagrams or tables.
[#319]: https://github.com/gotgenes/pi-packages/issues/319
[#322]: https://github.com/gotgenes/pi-packages/issues/322
[#323]: https://github.com/gotgenes/pi-packages/issues/323
[#326]: https://github.com/gotgenes/pi-packages/issues/326
[#327]: https://github.com/gotgenes/pi-packages/issues/327
[#329]: https://github.com/gotgenes/pi-packages/issues/329
[#330]: https://github.com/gotgenes/pi-packages/issues/330
[#331]: https://github.com/gotgenes/pi-packages/issues/331
## Stage: Implementation — TDD (2026-06-03T02:10:00Z)
### Session summary
Completed all three TDD cycles: (1) introduced `GateHandlerSession`, added it to `PermissionSession`'s `implements` list, rewired the handler constructor to accept `runner: GateRunner` and `session: GateHandlerSession`, updated all four call sites (`index.ts` + three test fixtures); (2) dropped the `as unknown as PermissionSession` casts by defining `MockGateHandlerSession` — an intersection of all required roles — and rewriting `makeSession` to use per-field `??` selection with `vi.fn<T>()` typed mocks; (3) updated `architecture.md` module-structure listing and marked Phase 3 Step 11 ✅.
Test count was 1807 before and after (behavior-preserving refactor).
### Observations
- The plan described the cast-removal approach as "spread `...overrides` last" but this pattern caused TypeScript issues when used with a type annotation on the const (spread of `Partial<T>` into `T` makes required fields optional).
Resolved by switching to the per-field `??` selection pattern already established in `gate-fixtures.ts` (`makeGateInputs`), which lets TypeScript verify each field individually against `MockGateHandlerSession[K]`.
- The `resolve` delegation calls `session.checkPermission(surface, input, agentName, session.getSessionRuleset())` with 4 arguments, but `GateHandlerSession.checkPermission` has only 3 params.
Resolved by adding a 4-arg `checkPermission` override in the inline type of `MockGateHandlerSession` (which overrides the 3-arg version from `GateHandlerSession` in the intersection); the handler's 3-arg call sites still compile because the 4th param is optional.
- `vi.fn<Signature>()` with the exact method type (e.g., `vi.fn<MockGateHandlerSession["activate"]>()`) ensures TypeScript checks the mock against the interface at creation, eliminating the need for any cast.
- `undefined as unknown as ExtensionContext` replaces the old `undefined as never` hack in the `canConfirm`/`promptPermission` delegations — cleaner and avoids the `never` TDZ issue.
- The `external-directory-integration.test.ts` had an unused `PromptPermissionDetails` import after the refactor (the type is now inferred from the `vi.fn<T>()` generic); removed in the Step 2 commit.
- Pre-completion reviewer verdict: WARN — one minor finding: the S11 Mermaid node in `architecture.md` was missing the ✅ marker carried by the completed S8/S9/S10 nodes.
Fixed in a follow-up `docs:` commit.
## Stage: Final Retrospective (2026-06-03T02:35:00Z)
### Session summary
Reviewed the full two-stage arc (Planning + TDD) for issue #325.
The TDD session executed all three plan steps cleanly across 90 turns on `claude-sonnet-4-6` with zero user corrections, zero rework, and one pre-completion `WARN` (a missing Mermaid ✅ marker, fixed in the same session).
The one substantive deviation — the plan's prescribed `{ ...defaults, ...overrides }` spread did not typecheck under a precise return annotation — was self-identified and resolved by adopting the existing `gate-fixtures.ts` per-field `??` pattern.
### Observations
#### What went well
- Thorough pre-implementation reconnaissance before Step 2: turns 3451 ran ~15 targeted `grep` calls to enumerate every `makeHandler({ session: … })` override key across all six handler test files before touching the shared `makeSession` type.
This confirmed no caller passed the vestigial `getToolPermission` / `config` keys, so dropping them was provably safe — no rework, no broken test surfaced later.
- Incremental verification: `pnpm run check` + package test suite ran after Step 1 (turns 3132) and again after Step 2 (turns 5758), with `lint` after each.
A type regression would have been caught at the step that introduced it, not at the end.
- Self-identified plan deviation handled cleanly: the plan's `{ ...defaults, ...overrides }` spread approach conflicts with the `testing` skill's known mock-typing pitfall.
The agent recognized this without being told and pivoted to the per-field `?? vi.fn<T>()` pattern already established in `gate-fixtures.ts` (`makeGateInputs` / `makeGateRunner`) — a novel win: the codebase's own convention resolved a plan-prescribed dead end.
#### What caused friction (agent side)
- `missing-context` (planning-side, not TDD) — the plan's Design Overview prescribed defining the delegations inline "then spread `...overrides` last," which does not typecheck once the const is annotated `MockGateHandlerSession` (spread of `Partial<T>` into `T` makes required fields optional).
Impact: no rework — the deviation was caught at design-read time and resolved in the first Step 2 write; cost was a few minutes of re-derivation.
The `testing` skill already warns the spread "erases mock methods," but it does not name the constructive alternative (per-field `??` + `vi.fn<T>()` + precise return annotation) nor connect it to the cast-removal use case.
- `other` (minor) — a transient unused `PromptPermissionDetails` import lingered in `external-directory-integration.test.ts` after the `vi.fn<T>()` generics made the explicit annotation unnecessary.
Impact: caught by `lint` immediately (turn 59), removed in the same step (turn 62); no rework beyond one edit.
#### What caused friction (user side)
- None.
The session ran end-to-end without user intervention, which is the expected shape for a behavior-preserving refactor with a complete plan.
No earlier-context opportunity applies.
### Diagnostic details
- **Model-performance correlation** — all 90 TDD turns ran on `claude-sonnet-4-6`, appropriate for mechanical-plus-type-level refactoring.
The single subagent dispatch (pre-completion-reviewer, turn 80) ran on its agent-frontmatter default model and produced a thorough 39-tool-use report; no model mismatch.
- **Escalation-delay tracking** — no `rabbit-hole` friction; no sequence exceeded 5 consecutive tool calls on the same error.
The longest same-purpose run (the turn 3451 grep sweep) was deliberate reconnaissance, not stuck-state thrashing.
- **Unused-tool detection** — the grep sweep used exact-symbol matching (`makeHandler({`, `checkPermission`), which is the correct tool; `colgrep` would not have improved exact-key enumeration.
No Explore/Plan dispatch was warranted.
- **Feedback-loop gap analysis** — verification was incremental (check/test after each of Steps 1 and 2, full suite + `fallow dead-code` + lockfile check after Step 3); no end-only verification gap.
### Proposed follow-ups
- Refine the `testing` skill to name the per-field `?? vi.fn<T>()` cast-removal pattern and its exception to the "do not annotate the return type" rule (the annotation is correct when callers supply pre-built mocks via overrides, which is what makes the completeness check enforce cast safety).
Deferred at the maintainer's direction — recorded here rather than applied inline.
### Changes made
1. Appended this Final Retrospective stage entry to `packages/pi-permission-system/docs/retro/0325-narrow-permission-gate-handler-roles.md`.
No prompt or `AGENTS.md` edits were made; the one proposed `testing`-skill refinement is recorded above as a deferred follow-up per the maintainer's choice.