--- issue: 569 issue_title: "Move the presentation family onto the tool-kind product" --- # Retro: #569 — Move the presentation family onto the tool-kind product ## Stage: Planning (2026-07-10T00:00:00Z) ### Session summary Planned Phase 10 Step 2 of the pi-permission-system roadmap: migrate the presentation family (`tool-preview-formatter.ts`, `permission-prompts.ts`, `denial-messages.ts`, `handlers/gates/helpers.ts::deriveDecisionValue`) onto the Step 1 `access-intent/tool-kind.ts` product, consolidating the private `denial-messages.ts::isMcpCheck` derivation into a single shared `isMcpCheck` alongside `classifyToolKind`. Wrote a five-step TDD plan (one true red for the new `isMcpCheck` unit tests, the rest behavior-preserving refactors under the existing green suite) and committed it as `docs/plans/0569-presentation-tool-kind-product.md`. ### Observations - **Shared `isMcpCheck` must keep the `source === "mcp"` disjunct.** `classifyToolKind(toolName)` keys purely on the tool name, but the presentation MCP-ness derivation also considers `source === "mcp"` — and `deriveSource` (Step 1) can set `source: "mcp"` on a result whose `toolName` is a server-qualified string. Two existing characterization tests pin this exact case (`denial-messages.test.ts` "MCP source with target on non-mcp toolName", `tool-preview-formatter.test.ts` "returns undefined for mcp source"), so a naive reduction to `classifyToolKind(...) === "mcp"` would regress. - **Target-presence separated from MCP-classification (SRP).** The old private `isMcpCheck` baked in `&& !!check.target`. Chose to make the shared predicate MCP-ness only (no target) and hoist `&& check.target` to the three denial sites and the one prompt site that need it (also gives TS truthy-narrowing); `tool-preview-formatter.ts` deliberately omits it, matching its original which had no target check. One predicate, uniform across all four files. - **Roadmap wording nuance.** The Step 2 roadmap entry says "delete `isMcpCheck`" — this means delete the *private* copy in `denial-messages.ts`; the plan promotes a shared `isMcpCheck` to `tool-kind.ts`. Noted so the Landed bullet phrases it as relocate-and-share, not a plain delete. - **Metric outcome.** After migration the recompute grep (`toolName === "(bash|mcp)"|source === "mcp"`) drops from 12 to an expected 2, both inside `tool-kind.ts` (the module docstring and the `source === "mcp"` disjunct in `isMcpCheck`) — within the Phase 10 end-state target of ≤ 4. Migrated `classifyToolKind(x) === "bash"` does not match the grep (the `)` before `===`), as confirmed by Step 1. - **`deriveDecisionValue` empty-path fallback.** The original `if (path) return path; return toolName` treats `""` as falsy; preserved with a truthy ternary (not `??`), pinned by `helpers.test.ts`. - **Release posture.** `refactor:` (hidden changelog type), **tail** of batch "tool-kind-dispatch" (head Step 1 / #568 landed) → ship now (land the batch tail); does not cut a release on its own, auto-batches into the next releasing change. - **First-party, unambiguous** → no `ask_user` gate; the one design choice (single no-target predicate) was resolved via code-design/SRP heuristics and existing characterization coverage. ## Stage: Implementation — TDD (2026-07-10T16:12:00Z) ### Session summary Executed the five-step plan: added the shared `isMcpCheck` to `access-intent/tool-kind.ts` and migrated the four presentation consumers (`denial-messages`, `permission-prompts`, `tool-preview-formatter`, `deriveDecisionValue`) onto `classifyToolKind`/`isMcpCheck`, then recorded the roadmap step in `architecture.md`. Four `refactor:` commits + one `docs:` commit; the suite grew 2317 → 2321 (+4, the new `isMcpCheck` unit tests). The recompute grep dropped from 12 to 2 (both inside `tool-kind.ts`: the docstring and the `isMcpCheck` `source === "mcp"` disjunct), within the Phase 10 end-state target of ≤ 4 — completing the "tool-kind-dispatch" batch (tail). ### Observations - **Pre-completion reviewer: PASS** — all deterministic checks green (`check`, root `lint`, 2321 tests, `fallow dead-code`), all three behavior-preservation focus areas verified (the `&& target` guard relocation, the preserved `source === "mcp"` disjunct, the exhaustive-switch empty-path fallback), 4 Mermaid charts re-rendered clean, cross-step invariants intact. No warnings. - **One in-step lint fixup (deviation, folded into Step 4).** `@typescript-eslint/prefer-nullish-coalescing` flagged the empty-path ternary `path ? path : toolName`. Replaced with `path || toolName` plus a documented `eslint-disable-next-line` (the testing skill's idiom), preserving the original `if (path)` truthiness so an empty-string `path` falls through to `toolName` — `??` would have returned `""` and changed behavior. - **No new characterization tests needed.** The four presentation characterization suites already pinned every branch — including the two critical `source === "mcp"`-disjunct tests (`denial-messages.test.ts` "MCP source with target on non-mcp toolName", `tool-preview-formatter.test.ts` "returns undefined for mcp source") — so each migration was a pure refactor under green with the test files unmodified. - **Reviewer's benign side-effect note.** Routing through `classifyToolKind` inherits its `toolName.trim()` (from Step 1), so the presentation sites now trim before comparing where they used bare `===`. Real tool names never carry surrounding whitespace, so this is not an observable change. - **Plan held exactly.** All touched files matched the Module-Level Changes list; the metric prediction (12 → 2) and the SRP design (single no-target `isMcpCheck` with `&& target` hoisted to call sites) landed as written. ## Stage: Final Retrospective (2026-07-11T03:00:54Z) ### Session summary One continuous session carried #569 through all four stages — plan, TDD, ship, retro — for Phase 10 Step 2 of the pi-permission-system roadmap, completing the "tool-kind-dispatch" batch. The implementation landed cleanly: five red→green→commit cycles (four `refactor:` + one `docs:`), suite 2317 → 2321 (+4 `isMcpCheck` unit tests), pre-completion PASS, CI green, issue closed, no release cut (all commits are `refactor:`/excluded-`docs:`, so the batch auto-defers to the next releasing change). One minor self-caught lint fixup; zero rework, zero user corrections, zero CI or reviewer failures. ### Observations #### What went well - **Plan-to-execution fidelity, second consecutive batch step.** Like #568, the plan's predictions held exactly: the SRP design (one no-target `isMcpCheck`, `&& target` hoisted to call sites), the metric (recompute 12 → 2, both inside `tool-kind.ts`), and the Test Impact Analysis (existing suites already pin every branch, no new characterization tests) all landed as written. The two `source === "mcp"`-disjunct characterization tests the plan called out (`denial-messages.test.ts` "MCP source with target on non-mcp toolName", `tool-preview-formatter.test.ts` "returns undefined for mcp source") were the exact safety net that made each migration a pure refactor under green. - **Fold-first-consumer mitigation worked again.** Introducing `isMcpCheck` and migrating `denial-messages.ts` in the same commit kept the export from ever landing unwired — `fallow dead-code` stayed green, as the plan's mitigation anticipated. #### What caused friction (agent side) - `other` (plan mispredicted the lint-safe form) — the plan's `deriveDecisionValue` sketch used a truthy ternary (`path ? path : toolName`) to preserve the empty-path fall-through, but `@typescript-eslint/prefer-nullish-coalescing` also flags the `x ? x : y` ternary, not just `||`. The commit hook rejected it; I switched to `path || toolName` + an `eslint-disable-next-line` (the testing skill's documented idiom). Self-identified (caught by the pre-commit gate). Impact: one extra edit + one eslint re-run within Step 4, no rework, no reorder. - `missing-context` (malformed read path) — an early planning read used `/Users/chris/development/pi/pi-permission-system/src/permission-manager.ts` (missing the `pi-packages/packages/` segment), which the permission system correctly denied as an external-directory access. Re-read with the correct `packages/pi-permission-system/...` path immediately. Impact: one denied tool call, no rework. #### What caused friction (user side) - None. User involvement was mechanical flow-approval only; no strategic redirection was needed and none was missing. The one permission denial was the extension doing its job on an agent-side path typo, not a user intervention. ### Diagnostic details - **Model-performance correlation** — one subagent dispatch: the `pre-completion-reviewer` on judgment-heavy review work (deterministic gates + behavior-preservation verification). Appropriate match; it ran the gates and returned a scoped PASS with accurate recompute numbers. - **Escalation-delay tracking** — no `rabbit-hole` friction; the lint error resolved in a single edit, no error sequence exceeded one tool call. - **Unused-tool detection** — nothing missed; `grep`/`read` covered the small exploration surface (this was a well-specified refactor with the plan already in hand). - **Feedback-loop gap analysis** — verification ran incrementally: `pnpm run check` after the shared-type step and again after Step 4, the affected test file per red→green cycle, then the full suite + root `lint` + `fallow dead-code` before the reviewer. No end-of-session-only verification. ### Changes made 1. `.pi/skills/testing/SKILL.md` (Operator semantics) — noted that `@typescript-eslint/prefer-nullish-coalescing` also flags the `x ? x : y` ternary, not only `||`, so a ternary cannot be used to dodge the rule; use `x || y` with the disable.