Files
my-pi/pi-permission-system/docs/retro/0337-dissolve-extension-runtime.md

101 lines
10 KiB
Markdown
Raw Permalink 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: 337
issue_title: "Dissolve ExtensionRuntime; one source of truth for session state"
---
# Retro: #337 — Dissolve ExtensionRuntime; one source of truth for session state
## Stage: Planning (2026-06-06T22:30:24Z)
### Session summary
Produced the implementation plan for Phase 4 Step 4: dissolving the `ExtensionRuntime` god object and unifying session state.
Confirmed Steps 13 (`#334`/`#335`/`#336`) are complete and that the transitional `RuntimeContextRef` seam was explicitly left for this step to retire.
Traced the split-brain through `index.ts`, `runtime.ts`, `config-store.ts`, `permission-session.ts`, `permission-event-rpc.ts`, `config-modal.ts`, and `permissions-service.ts`.
### Observations
- The codebase actually has *two* split-brains, not one.
The documented one is `PermissionManager` / `SessionRules`: the gate path uses a separate `sessionManager` and `PermissionSession`'s private `new SessionRules()`, while the RPC check / config-modal / `LocalPermissionsService` read `runtime.permissionManager` / `runtime.sessionRules`.
The second, quieter one is context: `ConfigStore` reads/writes `runtime.runtimeContext` via `RuntimeContextRef`, while `PermissionSession` owns its own private `this.context` — kept in sync only by `session_start` call order.
The plan closes both.
- Decided to split delivery into a `fix:` commit (share single instances, minimal structural change, runtime object still present) and a `refactor:` commit (dissolve the runtime, retire `RuntimeContextRef`, delete `runtime.ts`).
This isolates a real `fix:` (patch release) from behavior-preserving churn and keeps each commit green.
- `test/runtime.test.ts` is fully redundant: every path-derivation case already exists in `test/extension-paths.test.ts` against `computeExtensionPaths`; default-config in `config-store.test.ts`; logger wiring in `composition-root.test.ts`.
Deletes cleanly with no coverage loss.
- `makeSession` in `handler-fixtures.ts` is a duck-typed mock, not a real `PermissionSession`, so the new injected `SessionRules` constructor slot only affects `permission-session.test.ts` `createSession` and `index.ts` — not the gate-handler fixtures.
- `src/runtime.ts` can be deleted outright rather than left as a re-export shell: no module imports `ExtensionPaths` from it (consumers already import from `extension-paths.ts`).
- Characterization-test approach: drive a gate session-approval through the composition root with a UI `ctx` whose `ui.select` returns `options[1]` (label-agnostic "for this session"), then assert the RPC check and `getPermissionsService().checkPermission` both report `allow`.
Red on current code (RPC reads empty session rules), green after the fix.
- No `ask_user` needed — the issue's proposed change and the roadmap pin the design unambiguously.
## Stage: Implementation — TDD (2026-06-06T23:03:30Z)
### Session summary
Completed all three TDD steps: a `fix:` commit sharing one `PermissionManager` and `SessionRules` across the gate and RPC paths (bug fix), a `refactor:` commit dissolving `ExtensionRuntime` and retiring `RuntimeContextRef`, and a `docs:` commit updating the architecture roadmap plus marking Step 4 complete.
Test count: 1837 → 1838 (characterization test added in `composition-root.test.ts`) → 1815 (86 files; `runtime.test.ts` deleted, its 23 tests already covered by `extension-paths.test.ts`).
Pre-completion reviewer returned WARN; both findings were resolved inline.
### Observations
- The `prefer-const` / `@typescript-eslint/no-unnecessary-condition` lint conflict on the `session` forward reference was resolved by introducing a `sessionNotify: PermissionSession | null = null` holder.
The logger's notify closure uses `sessionNotify?.getRuntimeContext()` (correctly nullable), while `session` is declared `const` after `PermissionSession` is constructed.
This is cleaner than `null as unknown as PermissionSession` because optional chaining on the holder is safe, and there is no `as unknown as` cast to suppress.
- The `let configStore = null as unknown as ConfigStore` forward reference mirrors the pattern that existed in `createExtensionRuntime` and was retained (documented in a comment).
- Two context-seam tests in `config-store.test.ts` ("updates context via context.set", "does not overwrite context when ctx is omitted") were replaced with direct ctx-parameter behavior tests ("uses the passed ctx cwd for `loadAndMergeConfigs`", "uses empty string cwd when no ctx is provided").
No coverage lost; the `makeContextRef` helper and `RuntimeContextRef` import were removed.
- The `@typescript-eslint/no-deprecated` lint error on `PERMISSIONS_RPC_CHECK_CHANNEL` in the new composition-root test was fixed by extracting the channel value to a local `const rpcCheckChannel: string = PERMISSIONS_RPC_CHECK_CHANNEL` with a single `eslint-disable-next-line` annotation — cleaner than per-use suppressions.
- Reviewer WARN 1 (Step 4 not marked `✓ complete` in `architecture.md`) was addressed immediately with an additional `docs:` commit, per the package skill requirement to mark steps complete at ship time rather than deferring.
- Reviewer WARN 2 (pre-existing three-field cache reset in `permission-session.ts` without a `clearCaches()` helper) is a known smell documented in the Phase 4 plans; not introduced by this PR.
## Stage: Final Retrospective (2026-06-06T23:33:45Z)
### Session summary
Shipped Phase 4 Step 4 end-to-end across four stages (plan → TDD → ship → retro): dissolved the `ExtensionRuntime` god object, fixed the session-rules / `PermissionManager` split-brain, and released `pi-permission-system@v10.3.1`.
The plan was accurate enough that TDD execution matched it almost line-for-line; all friction was lint-driven micro-rework caught pre-commit by the verification loop.
No user corrections were needed beyond a single "Continue." nudge and a status check.
### Observations
#### What went well
- Model-task fit across the workflow was clean (see Diagnostic details): `opus` for plan/retro judgment, `sonnet` for implementation, and a `deepseek-v4-flash` model executing the deterministic `/ship-issue` workflow flawlessly — correctly escalating the one judgment point (stacked-release batch-vs-now) to the user via `ask_user` rather than deciding alone.
- The `sessionNotify: PermissionSession | null` nullable-holder pattern cleanly resolved the forward-reference + Biome/ESLint conflict in `index.ts` without an `as unknown as` cast on the session reference (the `configStore` ref kept the pre-existing `null as unknown as ConfigStore` idiom).
- Plan accuracy: the two-split-brain diagnosis, the `fix:` + `refactor:` split, and the zero-coverage-loss deletion of `runtime.test.ts` all played out exactly as the plan predicted — the planning-stage exploration (reading every consumer + `extension-paths.test.ts` overlap) paid off directly.
- The verification loop ran incrementally (`pnpm run check` after each interface change, targeted `vitest` for red/green, full lint+test before every commit), so every slip was caught before commit.
#### What caused friction (agent side)
- `instruction-violation` (self-identified) — the first `index.ts` rewrite used `configStore!.current()` (a `!` assertion) plus an `eslint-disable` for `@typescript-eslint/no-non-null-assertion`, despite AGENTS.md's "Biome / ESLint linter conflicts" rule banning `x!`.
`pnpm run lint` flagged it (5 errors: 2× `prefer-const`, the unused disable directive, 2× unnecessary optional chain); the fix was the nullable-holder rewrite.
Impact: one `index.ts` rewrite, caught pre-commit — no wasted commit, no shipped defect.
- `other` (self-identified) — appending the TDD stage entry to the retro via `Edit` produced a malformed file: the supplied `newText` was incomplete (ended mid-sentence) and the one-sentence-per-line autoformat interleaved it into the Planning observations.
Re-reading caught it; a full `Write` fixed it.
Impact: one retro rewrite.
Note: retro files accumulate repeated `### Observations` / `### Session summary` headers across stages, so `Edit` anchors on those headers are inherently ambiguous.
- `other` (self-identified) — two transient editing slips in `config-store.ts` / the deprecated-channel test: a dead `cwdOrNull` local (removed immediately) and `eslint-disable` comments placed on the wrong lines (refactored to a single local-const disable).
Impact: ~1 extra edit each; no rework beyond the same step.
#### What caused friction (user side)
- None of substance.
The user's involvement was light-touch oversight (one "Continue." after a turn ended without a trailing tool call, and one "Where are we?"
status check) rather than strategic correction — appropriate for a session executing a detailed pre-approved plan.
### Diagnostic details
- **Model-performance correlation** — Planning `anthropic/claude-opus-4-8` (judgment-heavy design tracing), TDD `anthropic/claude-sonnet-4-6` (implementation), Shipping `opencode-go/deepseek-v4-flash` (mechanical procedural workflow), Retro `anthropic/claude-opus-4-8` (synthesis).
No mismatches: the flash model on `/ship-issue` is the intended fit — a deterministic workflow where the sole judgment call was correctly delegated to the user.
The `pre-completion-reviewer` subagent ran on its own frontmatter model and produced a thorough WARN report (deterministic checks, acceptance criteria, code design, Mermaid parse) — quality adequate for the judgment-heavy review.
- **Escalation-delay tracking** — no rabbit-holes; the longest same-error sequence was the `index.ts` lint conflict at one lint run → one rewrite (well under the 5-call threshold).
- **Unused-tool detection** — no `missing-context` or `rabbit-hole` points; no subagent or `colgrep` opportunity was missed (planning-stage `grep`/`read` exploration was sufficient).
- **Feedback-loop gap analysis** — no gap; verification was incremental throughout, not end-loaded.
### Changes made
1. `.pi/prompts/tdd-plan.md` — added retro-append guidance: anchor the `Edit` on the file's last line or use `Write`, since repeated stage headers make header-anchored edits ambiguous.
2. `.pi/prompts/build-plan.md` — same retro-append guidance line.
3. `.pi/prompts/retro.md` — same guidance, added beside the existing "append the new entry" instruction in Step 3.