7.8 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 331 | Narrow AgentPrepHandler and SessionLifecycleHandler against role interfaces |
Retro: #331 — Narrow AgentPrepHandler and SessionLifecycleHandler against role interfaces
Stage: Planning (2026-06-03T22:30:12Z)
Session summary
Produced a four-step plan to retype AgentPrepHandler and SessionLifecycleHandler against narrow per-handler session role interfaces (AgentPrepSession, SessionLifecycleSession) instead of the concrete PermissionSession, completing the handler-narrowing arc started by [#325].
The plan reuses the existing two-method GateHandlerSession context role for AgentPrepHandler and drops the last two as unknown as PermissionSession casts in the handler test tree.
Observations
AgentPrepHandlercallsresolveAgentName(ctx, systemPrompt)(two args), butGateHandlerSession.resolveAgentNameis declared single-arg. Resolved by widening the role method to an optionalsystemPromptparameter — behavior-neutral for the gate handler and already present on the concrete method. Alternative (a separateAgentPrepSession.resolveAgentNamedeclaration) was rejected because the issue directs reusing the context role rather than redefining it.SessionLifecycleHandlerusesresolveAgentNamebut never callsactivate, so it deliberately does not reuseGateHandlerSession(that would carry an unused method — an ISP violation). Its role declaresresolveAgentNameindependently; the signature overlap withGateHandlerSessionis accepted as normal for role interfaces.AgentPrepHandlerpassesthis.sessiontoresolveSkillPromptEntries, soAgentPrepSessionextends the existingSkillPermissionCheckerrole (checkPermission) in addition toGateHandlerSession.- The current
before-agent-start.test.tsmock carries vestigialloggerandgetActiveSkillEntriesfields the handler never reads; the retyped literal must drop both or TypeScript's excess-property check rejects them once the cast is gone. - No
index.tswiring change is needed —PermissionSessionimplements the new roles, so it stays assignable to the narrowed constructor parameters. - Architecture doc already lists this as Phase 3 Step 14; the plan only needs to mark it ✅ and record the role names plus the
resolveAgentNamewidening. - Decided against extracting a shared
refreshConfigmicro-role (single shared method does not clear design-review check 7); declaring it on each role is cheaper than the wrong abstraction.
Stage: Implementation — TDD (2026-06-03T22:40:34Z)
Session summary
Implemented all four TDD steps: introduced AgentPrepSession and SessionLifecycleSession role interfaces, widened GateHandlerSession.resolveAgentName to accept an optional systemPrompt, added both roles to PermissionSession's implements list, retyped both handler constructors, and dropped the last two as unknown as PermissionSession casts in the handler test tree using the vi.fn<T>() per-field pattern.
No new tests were added (behavior-preserving refactor; existing suite plus pnpm run check was the safety net).
Test count held at 84 files / 1817 tests.
Observations
- Plan deviation: the
before-agent-start.test.tsmock'scheckPermissiondefault used{ state: "allow" }in the original, butPermissionCheckResultrequirestoolName,source, andorigintoo. Fixed by importingmakeCheckResultfrom the sharedhandler-fixtures.tsto build a complete default result — cleaner than duplicating the full shape inline. - The
vi.fn<AgentPrepSession["method"]>()pattern worked cleanly for all 11 methods across the two mocks; no union-type erasure issues because the??-per-field approach (not spread) was used throughout. - Pre-completion reviewer: PASS.
Reviewer WARN:
SessionLifecycleHandleraccessessession.logger.warn/debug— a two-hop Law of Demeter reach-through — noted as a pre-existing pattern intentionally carried forward (theSessionLifecycleSessionrole exposesreadonly loggerby design). No action required before/ship-issue.
Stage: Final Retrospective (2026-06-03T22:55:00Z)
Session summary
Completed issue #331 end-to-end across planning and TDD stages: introduced two narrow per-handler session role interfaces, widened GateHandlerSession.resolveAgentName, and dropped the last two as unknown as PermissionSession casts in the handler test tree.
Seven commits, zero rework beyond one type-checker-caught mock-payload fix, and a PASS from the pre-completion reviewer.
The session leaned heavily on the [#325] precedent (a nearly identical handler-narrowing refactor) as a template.
Observations
What went well
- Incremental verification was textbook:
pnpm run checkplus the per-filevitest runafter every TDD step, then the full suite +pnpm run lint+pnpm fallow dead-codeonce at the end. The mock-payload deviation surfaced at thepnpm run checkimmediately after the step-2 edit, not at the end — the feedback loop did exactly its job. - The [#325] precedent made planning fast and accurate: the plan reused the established
vi.fn<T>()per-field mock pattern and theMockGateHandlerSessionintersection idea verbatim, so the TDD stage hit no surprises in mock construction. - ISP judgment was applied deliberately rather than mechanically:
SessionLifecycleSessionomitsactivate(the handler never calls it) instead of reflexively reusing the fullGateHandlerSessioncontext role, and a one-methodrefreshConfigmicro-role was explicitly rejected against design-review check 7.
What caused friction (agent side)
missing-context(minor, self-identified) — the plan's mock sketch and the original test both usedcheckPermission: …mockReturnValue({ state: "allow" }). Theas unknown as PermissionSessioncast had masked that{ state: "allow" }is an incompletePermissionCheckResult(missingtoolName,source,origin); dropping the cast in step 2 surfaced it. Impact: ~2 extra tool calls (oneEditto importmakeCheckResult, one re-run ofpnpm run check); no rework beyond that, caught instantly by the type checker. Root: the plan's risk note anticipated a missing mock method ("a member the mock lacks") but the de-cast actually surfaced an incomplete return-value payload — a subtly different failure mode that the same fix (sharedmake*builder) addresses.
What caused friction (user side)
- None.
The user ran
/plan-issue,/tdd-plan, and/retroin sequence with no corrections. For a well-scoped refactor with a strong sibling precedent, mechanical oversight was appropriate — there was no strategic-judgment gap to surface earlier.
Diagnostic details
- Model-performance correlation — the
pre-completion-reviewersubagent ran onanthropic/claude-sonnet-4-6(judgment-heavy code review — appropriate). The parent session ran mostly onclaude-opus-4-8; a transientmodel_changetodeepseek-v4-flashappeared in the log, but the implementation completed cleanly and passed review, so no quality mismatch was observed. - Escalation-delay tracking — no rabbit-holes; the single deviation resolved in ~2 consecutive tool calls, well under the 5-call escalation threshold.
- Unused-tool detection — none warranted;
grepwas the right tool for exact-symbol matching during exploration, and the planning read-through was complete (handlers, role files, tests,index.ts, architecture doc). - Feedback-loop gap analysis — no gap; verification ran incrementally after each change rather than only at the end.
Changes made
.pi/skills/testing/SKILL.md— added a bullet under "Vitest mock patterns": dropping anas unknown as Xcast makes the type checker verifymockReturnValuepayloads, not just method presence; build incomplete return-value literals with the sharedmake*fixture builder.