--- issue: 321 issue_title: "Continue shared test-fixture extraction for the largest clone families" --- # Retro: #321 — Continue shared test-fixture extraction for the largest clone families ## Stage: Planning (2026-06-03T21:30:00Z) ### Session summary Produced a numbered build plan (`docs/plans/0321-continue-shared-test-fixture-extraction.md`) for migrating the four largest remaining test clone families onto the shared `test/helpers/` fixtures. Grounded the four families in a live `fallow dupes` run (133 clone groups, 7.6%) and confirmed by reading each file that all four already import the shared fixtures — the remaining clones are duplicate local factory definitions plus repeated override expressions, not unmigrated files. This is a pure test refactor (no `src/` changes), so the next stage is `/build-plan`, with migrate → full-suite-green → commit cycles. ### Observations - One `ask_user` design decision: how aggressively to extend the shared fixture API. User chose **both** — consolidate the duplicate factories AND add convenience shortcuts (`makeSurfaceCheck`, `makeBashCommandCheck`, `makeDenialDescriptor`, `makePathDispatchResolver`, a `makeGateRunner` `resolveResult` option, and a `makeHandler` `tools` shortcut) to hit the sub-6% target. - Key correctness risk identified: the local `makeSession` in `external-directory-integration.test.ts` diverges from the shared one in two defaults (`getInfrastructureReadDirs` → `[]` vs `["/test/agent", …]`; `checkPermission` → deny vs neutral allow). Analysis shows both differences are moot for that file's tests (explicit `checkPermission` everywhere; infra dirs never intersect test paths), but the full-suite green gate after the ext-dir step is the verification. - Applied the code-design "structural reasons before extracting duplication" heuristic to fence off genuine per-test intent that must stay inline: the per-agent `agentAwareCheck`, `toolName`-alias event literals, multi-condition path dispatch, and the bash regex/pattern values. - Discovered `makeTcc()` already defaults `input` to `{ command: "cat .env" }`, so many `bash-path.test.ts` clones collapse to a bare `makeTcc()` with no new helper. - The production refactors this step is "best sequenced after" ([#314], [#317]–[#320]) have all landed — the shared fixtures already import their outputs (`PermissionResolver`, `GateRunner`, the two pipelines, `GateDecisionReporter`), so no soft dependency blocks the work. - Carried forward the [#288] recurring friction as an explicit per-step instruction: grep each removed symbol before committing, because a stale value import passes `tsc` and the `lint` exit code but is a biome warning. - Scope guard: `external-directory-session-dedup.test.ts` shares the local-`makeSession` clone family but is the fifth file, outside the issue's named four; flagged as a conditional follow-up issue if the sub-6% target is missed, not scope creep here. [#288]: https://github.com/gotgenes/pi-packages/issues/288 ## Stage: Implementation — Build (2026-06-03T11:40:00Z) ### Session summary Completed all 5 build steps from the plan: runner gate migration (Step 1), bash-path gate migration (Step 2), tool-call handler migration (Step 3), external-directory integration migration (Step 4), and docs refresh (Step 5). Test count held steady at 86 files / 1834 tests throughout — pure refactor, no assertions changed. Pre-completion reviewer returned PASS. ### Observations - **Step 1** Fixed a TS2783 (`state` specified twice) in the `makeSurfaceCheck` implementation in `handler-fixtures.ts`; resolved by removing the redundant explicit `state: base.state` before the spread, letting `...base` cover it. One extra check+fix cycle. - **Step 2** A pre-commit eslint hook reformatted `gate-fixtures.ts` on the first commit attempt (exit 1); re-staged the auto-fixed file and committed cleanly. - **Steps 3–4** `makeSurfaceCheck` and `makeExtDirCheck` (a local thin wrapper in `external-directory-integration.test.ts`) replaced the surface-dispatch boilerplate cleanly; no assertion changes needed. The shared `makeSession` default `getInfrastructureReadDirs` (`[\u201c/test/agent\u201d, ...]`) did not intersect any ext-dir test path, confirming the planning analysis. - **Target miss**: duplication landed at 6.6% (122 clone groups), not under 6%. The remaining gap is the `external-directory-session-dedup.test.ts` family (local `makeSession`/`makeToolRegistry` clones across ext-dir + session-dedup + handler-fixtures), which was out of the four-file scope. A follow-up issue should be filed per the plan’s Open Questions. - No stale imports or `GateDescriptor`/`makeCheckPermission`/`makeDenialContextDescriptor` leaks found at any step. - **Reviewer verdict**: PASS — all deterministic checks green, new helpers documented in `SKILL.md`, architecture roadmap updated. [#314]: https://github.com/gotgenes/pi-packages/issues/314 [#317]: https://github.com/gotgenes/pi-packages/issues/317 [#320]: https://github.com/gotgenes/pi-packages/issues/320 ## Stage: Final Retrospective (2026-06-03T22:30:00Z) ### Session summary Single-day execution of the full lifecycle (plan → build → ship) for the four-family test-fixture extraction. All 5 build steps landed green with the suite holding at 86 files / 1834 tests, duplication dropped 7.6% → 6.6% (clone groups 133 → 122), and the pre-completion reviewer returned PASS. No release-please PR was opened because the change is entirely `test:`/`docs:` commits; the issue closed cleanly with no version bump. ### Observations #### What went well 1. The [#288] recurring friction — stale imports after deleting local factory definitions — was carried forward from the prior retro into the [#321] plan as an explicit per-step instruction (“grep each removed symbol before committing”). The build session then had **zero** stale-import slips: every deletion step ran a verifying `grep` (msgs 38, 47, 62) and found only legitimate survivors (`makeResolver`) or doc-comment references. This is a prior retro observation closing the loop — a documented friction pattern eliminated by a planning adjustment. 2. The upfront `ask_user` in planning (one decision: “both” — consolidate factories AND add convenience shortcuts) produced zero design churn across all 5 build steps; every helper the plan named was used as specified. 3. Incremental verification was clean: `pnpm run check` + `vitest run` ran after every build step (msgs 37, 46, 53/55, 61, 74), so each commit left the suite green with no broken-baseline commits. 4. The `code-design` “structural reasons before extracting duplication” heuristic was applied at plan time to fence off genuine per-test intent (per-agent `agentAwareCheck`, `toolName`-alias events, multi-condition path dispatch), so no shared helper became a discriminator-laden leaky abstraction. #### What caused friction (agent side) 1. `other` (tooling) — a `fallow dupes --json` attempt during planning (msg 13) returned exit 2 with an empty file; `fallow` also truncates its plain-text output to the top 10 clone groups, so a follow-up `tee` to capture the full list also came up short (msg 15, error). The agent recovered by reading the four target files directly instead of relying on `fallow`'s per-file clone breakdown. Impact: ~3 extra exploratory tool calls in planning; no rework, and the direct reads were the higher-fidelity path anyway. 2. `other` (mechanical) — a TS2783 (`state` specified more than once) in the new `makeSurfaceCheck` (msg 53): the explicit `state: base.state` was redundant with the trailing `...base` spread. Caught immediately by the post-step `pnpm run check`, fixed in one edit (msg 54). Impact: 1 extra check+fix cycle (~2 tool calls); no rework beyond the single line. 3. `other` (tooling) — the first commit of step 2 (msg 48) failed because the pre-commit eslint hook reformatted `gate-fixtures.ts` (import sort); re-staging the auto-fixed file and re-committing succeeded (msg 50). Impact: 1 extra add+commit cycle; no rework. #### What caused friction (user side) 1. None substantive. The two `Continue.` nudges in the build session (msgs 42, 45) were mechanical pacing prompts, not redirections — the work was on-track (mid-step-2 migration) at each. #### Estimation gap (not friction) 1. The plan's stated target was duplication < 6%; the realized figure was 6.6%. The gap was foreseen in planning (`external-directory-session-dedup.test.ts` was explicitly scoped out as a fifth family) and handled correctly at build time — the architecture roadmap records the realized 6.6%, and the residual session-dedup family is flagged for a follow-up issue. No correction needed; this is an accurate-estimate-with-documented-shortfall, not a miss. ### Diagnostic details - **Model-performance correlation** — no mismatches. Planning + retro ran on `claude-opus-4-8` (judgment-heavy: design decision, plan synthesis, cross-stage retro), the build on `claude-sonnet-4-6` (mechanical migration with type-checking), the pre-completion reviewer subagent on `anthropic/claude-sonnet-4-6` (judgment-heavy review), and shipping on `opencode-go/deepseek-v4-flash` (deterministic checklist). Each model matched its task complexity; the cheap flash model on the mechanical ship checklist is appropriate cost optimization. - **Escalation-delay tracking** — no `rabbit-hole` friction points; the two mechanical issues (TS2783, eslint hook) each resolved in a single cycle, well under the 5-call threshold. - **Unused-tool detection** — `colgrep` was not used despite the planning prompt recommending it, but the agent knew exact symbol names (`makeSession`, `makeCheckPermission`, etc.), so `grep` and direct file reads were the correct lower-latency choice; no missing-context friction resulted. - **Feedback-loop gap analysis** — no gaps. Verification ran incrementally after each of the 5 build steps, not just at the end. ### Changes made 1. Appended this Final Retrospective stage entry to `packages/pi-permission-system/docs/retro/0321-continue-shared-test-fixture-extraction.md`. No prompt or `AGENTS.md` changes — the user chose retro-file-only; the single tooling candidate (a `fallow dupes` truncation/`--json` note) was a single self-recovered instance below the bar for a new rule, recorded here only.