--- issue: 107 issue_title: "refactor: break handleToolCall into per-gate functions" --- # Retro: #107 — break handleToolCall into per-gate functions ## Final Retrospective (2026-05-07T00:30:00Z) ### Session summary Extracted four permission gates from a ~600-line `handleToolCall` into `src/handlers/gates/`, added 44 per-gate unit tests, removed 18 redundant integration tests, and wired the orchestrator as a ~30-line chain. Also added an npm→pnpm shim via `mise.toml` after discovering the project had no enforcement of its declared package manager. Released as v5.4.0. ### Observations #### What went well - The TDD cycle was clean: red→green→commit for each gate, with existing integration tests providing a safety net during the final wiring step. All 1165 (later 1147) tests passed at every checkpoint. - The user's "why do we need deep mocking?" question surfaced the real design issue (`ExtensionRuntime` as a god object, #111) rather than letting us paper over it with `Record`. This is a good example of asking "why" to get past the surface symptom. - The npm shim pass-through for `npm root` was a pragmatic solution that let us enforce pnpm without breaking our own startup path. #### What caused friction (agent side) 1. `instruction-violation` — Used `npm run build`, `npx vitest run`, and `npm run lint:all` throughout all 9 TDD steps despite the project using pnpm exclusively (`pnpm-lock.yaml`, `"packageManager"` in `package.json`). User-caught after all steps were complete. Impact: no functional breakage (scripts are runner-agnostic), but undermines the pnpm enforcement the session itself added. Root cause: `AGENTS.md` and all prompt templates (`tdd-plan.md`, `build-plan.md`) said `npm`/`npx`, and no rule said otherwise. 2. `instruction-violation` — Introduced `Record` in gate test factories to work around deep mock typing, violating the "avoid `any`" rule in `AGENTS.md`. User-caught. Impact: one extra `style:` commit (`eeb9d20`) to replace with `Record`. 3. `rabbit-hole` — First attempt at the npm shim destroyed positional parameters with `set -- $PATH`, causing `npm root -g` to silently return the wrong path (local instead of global). Self-identified on test. Impact: two revisions of the shim script before it worked correctly. 4. `missing-context` — Tried to use `isToolCallEventType` from the Pi SDK inside extracted gates by reconstructing a fake event object. The SDK checks `event.toolName` but the reconstructed event used `event.name`. Self-identified during TDD red→green. Impact: minor — removed the SDK call in favor of a direct `tcc.toolName` check, which is simpler anyway. 5. `missing-context` — Forgot `[env]` section header in `mise.toml`, causing `_.path` to have no effect. User-caught after restarting Pi. Impact: one round-trip of "restart Pi → still broken → fix config → restart again." #### What caused friction (user side) - The `npm` vs `pnpm` issue could have been caught earlier if the project had established the pnpm rule in `AGENTS.md` before this session. The user noticed it organically mid-session, which led to the productive shim work, but the TDD steps had already landed 9 commits using `npm`. - The user's sequential "why" questions (deep mocking → `ExtensionRuntime` → existing issues) were highly effective at reaching root cause. This pattern of redirecting from symptom to cause saved us from filing a narrow issue (#114) when the real target (#111) already existed. ### Changes made 1. `AGENTS.md` — added pnpm-over-npm rule in § Code Style; fixed `npm run build` → `pnpm run build` and `npx vitest run` → `pnpm vitest run` in § Testing. 2. `.pi/prompts/tdd-plan.md` — replaced all `npx vitest run`, `npm run build`, `npm run lint:all`, `npm run lint:fix` with `pnpm` equivalents. 3. `.pi/prompts/build-plan.md` — same replacements. 4. `.pi/prompts/README.md` — replaced `npm` test/lint script references with `pnpm`.