Files
my-pi/pi-permission-system/docs/retro/0335-extract-config-store.md
T

97 lines
9.1 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: 335
issue_title: "Extract a ConfigStore from the runtime free-functions"
---
# Retro: #335 — Extract a ConfigStore from the runtime free-functions
## Stage: Planning (2026-06-05T01:50:00Z)
### Session summary
Produced the numbered implementation plan for Phase 4 Step 2 — extracting a `ConfigStore` class that owns `config` + `lastConfigWarning` and converts the three `(runtime, …)` config free functions into methods.
Predecessor #334 (inject a single `PermissionManager`) is already merged; this is the first session on #335 (no prior retro).
The plan is six behavior-preserving TDD cycles using a lift-and-shift migration (introduce the store, back the runtime with it via a temporary `get config()` getter, migrate consumers one at a time, then delete the free functions).
### Observations
- The one genuine design fork — how the store obtains the runtime context that `refresh` / `logResolvedPaths` need — was settled by the roadmap, not by `ask_user`.
`docs/architecture/architecture.md` deliberately scopes the store to `config` + `lastConfigWarning` (context unification onto `PermissionSession` is Step 4 / #337), so owning `runtimeContext` in the store was rejected.
The plan injects a transitional `RuntimeContextRef` (get/set) backed by the still-runtime-owned `runtimeContext` field; it dissolves in #337.
- Tension between the issue's "the 4× `() => runtime.config` closures … are gone" and the roadmap reserving the `index.ts` deps-bag collapse for Step 5 (#338).
Resolved by removing the `PermissionSession`, `PermissionPrompter`, and command `() => runtime.config` closures here (those consumers hold the store / a `ConfigReader`), while the logger `getConfig` adapter is redirected to `configStore.current()` and fully retired in Step 3 (#336), and the forwarding `shouldAutoApprove` adapter survives to Step 5.
Documented as an explicit scoping decision in the plan's Non-Goals / Open Questions.
- The logger ↔ config temporal coupling (the store must be built before the logger whose sink the store defers to) is preserved verbatim — it is the existing deferred-binding pattern and Step 3 (#336) removes it.
Avoided pulling that fix forward into this Step.
- `createSessionLogger(runtime)` reads `runtime.writeDebugLog` / `writeReviewLog` / `runtimeContext` but **not** `runtime.config`, so removing the `config` field does not touch it — confirmed by grep before finalizing the module list.
- Lift-and-shift chosen because the alternative (remove `config` + the free functions in one commit) would force every consumer and test into a single oversized commit; the temporary `get config()` getter keeps `index.ts` compiling across the four consumer-migration steps.
## Stage: Implementation — TDD (2026-06-05T22:25:00Z)
### Session summary
All 6 TDD cycles completed across `config-store.ts` (new), `runtime.ts`, `permission-session.ts`, `permission-prompter.ts`, `config-modal.ts`, and `index.ts`.
Test count went from 1831 (86 files) to 1840 (87 files): +22 new `config-store.test.ts` tests, -13 deleted `refreshExtensionConfig` tests from `runtime.test.ts`.
Pre-completion reviewer returned PASS with no findings requiring action.
### Observations
- Step 2 (`runtime.ts`) required `Object.defineProperty` for the transitional `get config()` getter bridge; the plain-object-literal factory pattern can't have getters inline.
The `as unknown as ExtensionRuntime` cast is needed because `Object.defineProperty` returns the pre-cast type.
The bridge was removed cleanly in Step 6 when the last consumer (`canResolveAskPermissionRequest` in `index.ts`) migrated.
- `@deprecated` JSDoc tags on the Step-2 delegator functions triggered `@typescript-eslint/no-deprecated` on every call site in `index.ts`.
Replaced with prose comments per the AGENTS.md lift-and-shift rule ("do not mark it `@deprecated`").
- `ConfigStore.save` was flagged as dead code by fallow because fallow cannot trace calls through the `CommandConfigStore` interface.
Suppressed with `// fallow-ignore-next-line unused-class-member` plus an explanatory prose comment above it.
- Step 5 (`config-modal.ts`) needed two passes: the batch edit for the interface + import was rejected (edit[2] in the first call failed to match), but the body-method edits applied.
Lesson: when one edit in a batch fails, the entire batch is rolled back — check match fidelity for every oldText before sending.
- Architecture doc updated: `config-store.ts` added to the module-structure listing; Steps 1 and 2 marked `✓ complete` in the Phase 4 roadmap.
- Pre-completion reviewer verdict: **PASS** — all deterministic checks green, no structural findings, `fallow` clean.
## Stage: Final Retrospective (2026-06-05T10:56:41Z)
### Session summary
Shipped #335 end-to-end: the TDD implementation (6 green cycles), pre-completion review (PASS), push, CI verification, issue close, and release of `pi-permission-system-v10.3.0`.
The refactor is behavior-preserving — `config` now has one owner (`ConfigStore`) and the `() => runtime.config` closures plus three `(runtime, …)` free functions are gone.
Friction was concentrated in mechanical `Edit`-tool usage (failed batches, a clumsy large-block deletion), not in design or strategy.
### Observations
#### What went well
- Narrow-interface segregation (`ConfigReader` / `SessionConfigStore` / `CommandConfigStore`) let every test double satisfy the type structurally — zero `as unknown as` casts in the new fixtures.
The pre-completion reviewer called this out explicitly; it validates the `code-design` "inject a narrow interface, not the concrete class" rule.
- Incremental verification: `check` + `vitest` + `lint` ran after each of the 6 TDD steps, so the `@deprecated` lint error and the batch-application gaps surfaced inside the step that caused them rather than at the end.
- The `ask_user` batching gate at ship time worked cleanly — surfaced that #335 is 1 of 9 steps and let the user choose release-now vs. batch instead of assuming.
#### What caused friction (agent side)
- `wrong-abstraction` — deleting the ~145-line `refreshExtensionConfig` `describe` block from `runtime.test.ts` was attempted with incremental surgical `Edit`s (rename to `PLACEHOLDER_DELETED`, add `describe.skip`, add `eslint-disable-line` comments) before abandoning that for a one-shot `python3` truncation at a marker.
Impact: ~5 fumbling tool calls before switching to the bulk approach; no rework to shipped code.
- `other` (tool batch atomicity) — in both Step 4 (`permission-prompter.test.ts`) and Step 5 (`config-modal.ts`) a large multi-`edit` `Edit` call was rejected because one `oldText` (edit[2]) failed to match; the whole batch rolled back, but follow-up calls only re-applied a subset, so the import/interface edits silently never landed and a later `pnpm run check` failed.
Impact: 2 extra check-fix cycles (one per step).
- `instruction-violation` (self-identified) — marked the Step-2 delegator functions `@deprecated` despite the `testing` skill's explicit "do not mark it `@deprecated`" rule; `@typescript-eslint/no-deprecated` caught it at the Step-2 lint gate.
Impact: one extra edit cycle (3 JSDoc→prose edits); rule already exists and lint enforced it, so the safety net held.
- `missing-context` (user-caught) — the Phase 4 roadmap step-completion markers in `architecture.md` were not updated when #335 shipped (nor was #334's when it shipped earlier); the user had to ask "mark step 1 as complete."
Impact: one extra user round-trip + a follow-up commit (`bc0fd5f5`).
#### What caused friction (user side)
- None material.
The one user intervention (marking roadmap steps complete) was mechanical oversight of a doc-bookkeeping gap, not strategic redirection — the kind of thing the ship flow should catch automatically.
### Diagnostic details
- **Escalation-delay tracking** — the `runtime.test.ts` block deletion ran ~5 consecutive `Edit`/`Read` calls on the same goal before switching tactics; just under the 5-call dispatch threshold, but a single boundary-to-boundary replacement (or the `python3` cut) should have been the first move.
- **Feedback-loop gap analysis** — no gap: verification was incremental after every step, and `fallow dead-code` ran from the repo root at Step 6 and again at ship.
This is why the `@deprecated` and batch-application slips cost a cycle each instead of compounding.
- **Model-performance correlation** — the only subagent dispatch was the `pre-completion-reviewer` on its default model for judgment-heavy code review; appropriate, returned a thorough PASS.
No mismatch.
### Changes made
1. `.pi/skills/package-pi-permission-system/SKILL.md` — added a note to mark a roadmap step `✓ complete` in `architecture.md` as part of the shipping change (addresses the user-caught completion-marking gap).
2. `AGENTS.md` — added an `### Edit tool batches` subsection under Workflow: a rejected multi-edit `Edit` call applies nothing, so re-apply every intended edit and run `pnpm run check` after a rejection (addresses the Step 4 / Step 5 batch-atomicity friction).
3. `packages/pi-permission-system/docs/retro/0335-extract-config-store.md` — this Final Retrospective stage entry.