8.8 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 327 | Extract a ToolCallGatePipeline collaborator that owns tool-call gate construction |
Retro: #327 — Extract a ToolCallGatePipeline collaborator that owns tool-call gate construction
Stage: Planning (2026-06-03T03:45:47Z)
Session summary
Produced the implementation plan for extracting a ToolCallGatePipeline collaborator that owns tool-call gate construction, narrowing PermissionSession with getToolPreviewLimits() / getInfrastructureReadDirs(), and removing the anemic getInfrastructureDirs / getInfrastructureReadPaths getters.
The plan is a five-step lift-and-shift (add session methods → introduce pipeline + tests → inject and delegate → remove dead getters → docs), all behavior-preserving.
Confirmed #326 (handleInput unification) is already landed, so the handler's handleInput is unchanged here.
Observations
- Settled the
evaluate(...)seam the issue left open: choseevaluate(tcc, runner)with the pipeline owning the bash-command extraction and the singleBashProgram.parse, since those are purely tool-call gate-construction inputs thathandleInputnever needs (decided viaask_user). - The user corrected an initial draft that constructed the pipeline inside the
PermissionGateHandlerconstructor — that violated dependency injection. Revised soindex.tsconstructs the pipeline and injects it; the handler also drops its now-unneededcustomFormattersconstructor parameter. Deliberately left the pre-existingnew GateRunner(...)/new GateDecisionReporter(...)construction in the handler constructor alone — relocating those is the explicit scope of #320 and #325, and folding them in would balloon the issue. - Chose a narrow pipeline-owned interface
ToolCallGateInputs(extendsPermissionResolver) over depending on the concretePermissionSession, so the new pipeline unit tests stay cast-free. Avoided a layer inversion by not declaringPermissionSession implements ToolCallGateInputs— the structural check lives at thenew ToolCallGatePipeline(session, ...)call site, keeping the domain module free of an upward import from the handler layer. - The runner is passed per-call to
evaluaterather than injected into the pipeline, because the sameGateRunnerinstance is shared withhandleInput. - Key follow-on risk for
/tdd-plan: the session mocks are cast viaas unknown as PermissionSession, so renamed/added methods (getInfrastructureReadDirs,getToolPreviewLimits) fail at runtime, not at typecheck — step 3 must update every session mock on the handler/pipeline path and run the full suite.
Stage: Implementation — TDD (2026-06-03T04:09:24Z)
Session summary
All 5 TDD steps completed across 5 commits.
Added 14 tests (1796 → 1807 after removing the 3 deleted old-getter tests, then +14 new = 1807 net; old 2 old-getter tests subtracted).
ToolCallGatePipeline with ToolCallGateInputs interface introduced; makeGateInputs added to gate-fixtures.ts; handler and composition root wired correctly.
Pre-completion reviewer returned PASS.
Observations
- Step 3 risk materialised exactly as predicted:
getInfrastructureDirsandgetInfrastructureReadPathsoverrides intest/handlers/external-directory-integration.test.ts,external-directory-session-dedup.test.ts, andtool-call-events.test.tswere dead after the handler stopped calling them. Updating all mocks and running the full suite caught this correctly (no typecheck errors, but runtime failures if mocks were missed). external-directory-session-dedup.test.tshad 6 directnew PermissionGateHandler(...)calls; added a localmakeHandlerForSession(session)helper and replaced them all withperlin-place substitution — cleaner than 6 individual edits.- The
PermissionResolverimport in the new pipeline test file was unused (lint caught it) — removed before commit. - The
makeHandlerForSessionhelper in the dedup test file referencesmakeToolRegistry()which is defined after it; both arefunctiondeclarations so hoisting keeps them safe. - Pre-completion reviewer: PASS — no warnings.
Stage: Final Retrospective (2026-06-03T04:11:51Z)
Session summary
Planned and implemented #327 across three stages (planning, TDD, retro) in a single working session: extracted ToolCallGatePipeline (owning tool-call gate construction and the run loop), narrowed PermissionSession with getToolPreviewLimits() / getInfrastructureReadDirs(), and removed the anemic getInfrastructureDirs / getInfrastructureReadPaths getters.
Five behavior-preserving commits plus docs; the full suite went 1796 → 1807 tests, and the pre-completion reviewer returned PASS with no warnings.
The only substantive correction came in planning — a dependency-injection misstep the user caught before any code was written.
Observations
What went well
- The planning-stage risk note ("step 3 must update every session mock on the handler/pipeline path") fired exactly as predicted in TDD step 3, and was pre-mitigated — the renamed
getInfrastructureReadDirs/ addedgetToolPreviewLimitsmocks across three test files were updated in one pass with zero rework. The cross-session retro bridge worked as designed: a risk recorded at planning prevented a runtime-only (non-typecheck) failure at implementation. - The
ask_usergate on theevaluate(...)seam shape produced a decision (evaluate(tcc, runner), pipeline owns the bash parse) that held unchanged through implementation — no seam churn. - Lift-and-shift sequencing (add new methods alongside old → introduce pipeline → inject and delegate → remove old getters) kept every one of the five commits green and type-clean; no commit left the tree broken.
What caused friction (agent side)
instruction-violation— the initial plan draft constructedToolCallGatePipelineinside thePermissionGateHandlerconstructor (new ToolCallGatePipeline(...)), violating thecode-designskill's dependency-injection rule even though that skill was loaded. Root cause: anchored on local precedent — the handler already constructsGateRunnerandGateDecisionReporterinternally — without recognizing that this precedent is the exact smell #320 / #325 exist to remove. User-caught. Impact: design correction at planning before any code was written, so no code rework; the plan's Design Overview and TDD steps were revised to inject fromindex.tsand drop the handler'scustomFormattersparam.other— the plan used reference-style issue-link definitions ([#319]:…) with bare#319body references, trippingrumdlMD053 (unused link definition) on firstlint:md. Self-caught via lint; fixed with oneperlpass bracketing the body references. Impact: one extra fix cycle in planning, no rework. Themarkdown-conventionsskill already documents this rule, so no convention change is warranted.
What caused friction (user side)
- During TDD step 3 and the docs step, execution paused after tool calls and the user had to nudge three times ("You need to keep going", "Please continue").
Opportunity, not criticism: these were mid-step boundaries on a long mechanical refactor (handler +
index.ts+ four test files), not decision points — the continuation was unambiguous. No prompt or convention change proposed; this reads as turn-continuity friction rather than a workflow gap.
Changes made
- Added a one-sentence clause to the Dependency Inversion (DIP) section of
.pi/skills/code-design/SKILL.md: when adding a new collaborator to a class that still constructs other collaborators internally, inject the new one anyway — existing constructor-internal construction is often the smell being removed, not a precedent to extend. This addresses the user-caught DI violation where the plan draft constructedToolCallGatePipelineinside thePermissionGateHandlerconstructor by mirroring the siblingGateRunner/GateDecisionReporterconstruction.
Diagnostic details
- Model-performance correlation — one subagent dispatched (
pre-completion-reviewer, 236s, 34 tool uses) on judgment-heavy review work; appropriate match, no mismatch. Planning exploration (~15read/grepcalls) ran on the parent session rather than via anExploresubagent — acceptable here since the symbol set was known and keeping context aided the design decision. - Escalation-delay tracking — no
rabbit-holefriction; no error or approach occupied more than one or two consecutive tool calls. - Feedback-loop gap analysis — verification ran incrementally: each TDD step ran its affected test file (red → green) then
pnpm run check; step 3 ran the full handler test directory plus the whole suite andcheckbefore commit; final gates (full suite,check,lint,fallow dead-code) all green. No end-loaded-verification gap.