--- 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 1–3 (`#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.