13 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 341 | Slim PermissionSession to a state/lifecycle owner; unwind the fig-leaf interfaces |
Retro: #341 — Slim PermissionSession to a state/lifecycle owner; unwind the fig-leaf interfaces
Stage: Planning (2026-06-07T18:39:38Z)
Session summary
Produced the numbered plan for Phase 4, Step 8 — the final Track C step that slims PermissionSession to a state/lifecycle owner.
Confirmed all three prerequisites are CLOSED (Step 1 #334, Step 6 #339, Step 7 #340) and read the session/resolver/runner/handlers/fixtures source plus the #340 retro to pick up cross-session context.
Surfaced the one genuine design ambiguity via ask_user; the user chose Option A (retire the three handler interfaces, depend on the concrete PermissionSession, build real instances in tests).
Observations
- The headline "
GateRunner(session, session, session)→ three different collaborators" was already two-thirds done by Steps 6–7: the runner isGateRunner(resolver, session, gateway, reporter), so only the recorder role is still the session. The clean win is moving the recorder toSessionRules(renamerecord→recordSessionApproval,implements SessionApprovalRecorder); the runner call site is unchanged, only the injected object differs. - Scope is larger than the issue's 3-file headline implies.
The
#340retro is explicit that Step 8 also removes the session's transitional query duplicates (checkPermission,getToolPermission,getConfigIssues,getPolicyCacheStamp) and rewiresAgentPrepHandler+SessionLifecycleHandlerto the resolver.getSessionRulesetis also dead in production (no caller since#340— verified by grep) and is removed. PermissionGateHandlerdoes not gain a resolver dependency — itsGateRunneralready owns the resolver; it only needs the session'sactivate/resolveAgentName. Only the two non-gate handlers gain a concretePermissionResolverparameter.- Option A is a conscious trade-off against the package's "narrow interface, not concrete class" convention.
It is justified because Step 1 made the session/resolver constructible, so tests build real instances (no casts) — the convention's mock-cast smell does not reappear.
ScopedPermissionResolver,ToolCallGateInputs,SkillInputGateInputs, andSkillPermissionCheckerstay narrow. - The 104
makeHandlercall sites only break if its override-bag keys or return shape change —handler-fixtures.tsuses its ownMockGateHandlerSessionmock, not the real class, so removing methods fromPermissionSessiondoes not touch them. The plan preservesmakeHandler's override surface to keep Step 5's blast radius to the fixture file itself. - The existing
createSessionfactory inpermission-session.test.tsis the real-session fixture to promote intotest/helpers/session-fixtures.ts; the hand-rolled stateful recorder inexternal-directory-session-dedup.test.tscollapses into a realSessionRules+ real resolver sharing one ruleset. - RPC (
permission-event-rpc.ts) usespermissionManager.checkPermissiondirectly and onlysession.getRuntimeContext();config-modal.tsonly readssession.lastKnownActiveAgentName— neither blocks the query-method removals. - TDD order is lift-and-shift: promote the fixture, move the recorder, then retire one interface per commit (each deletion + handler retype + consumer-test rewrite folded together), then rebuild the gate-handler fixture, then docs.
Stage: Implementation — TDD (2026-06-07T20:05:00Z)
Session summary
Executed all six planned TDD steps plus docs: promoted the real-session fixture to test/helpers/session-fixtures.ts, moved the recorder role to SessionRules (record → recordSessionApproval, implements SessionApprovalRecorder), retired SessionLifecycleSession / AgentPrepSession / GateHandlerSession one per commit (rewiring AgentPrepHandler and SessionLifecycleHandler to a concrete PermissionResolver), rebuilt makeHandler on real session + resolver + SessionRules recorder, and updated architecture + skill docs.
Test count moved 1828 → 1823 (net −5: removed 6 PermissionSession delegation tests + 2 recorder/ruleset delegation tests, added 1 SessionApprovalRecorder conformance test on SessionRules; the remaining delta is the dedup-test rewrite collapsing onto real collaborators).
Pre-completion reviewer: PASS.
Observations
- The plan held well; the lift-and-shift order kept the suite green at every commit and the predicted "104
makeHandlercall sites stay put" was correct — only three handler-test assertions needed edits (session.activate→forwarding.startintool-call/input, andsession.checkPermission→permissionManager.checkPermissionininput), becausemakeHandlerpreserved its override-bag surface (routingcheckPermissionoverrides to the fake manager and session-state overrides tovi.spyOn). - Biggest unplanned discovery (surfaced by the user mid-step): after Step 5 removed the last
implements,fallowflagged fourPermissionSessionmembers (getActiveSkillEntries/getInfrastructureReadDirs/getToolPreviewLimits/lastKnownActiveAgentName). Root cause:fallowkeys member liveness offimplementsclauses, so the structurally-consumed members went dark when the fig-leaf interfaces left. Resolved truthfully for the trio by declaringPermissionSession implements ToolCallGateInputs(a genuine pipeline-input contract, no import cycle — the pipeline does not import the session); this is now reflected in the plan's design but was not in the original Module-Level Changes. ForlastKnownActiveAgentName, a named-interface attempt (ActiveAgentNameReader) did not satisfyfallow— the blind spot is the object-literal wiring inindex.ts(config-modal receivessessionas an object-literal property, not a traced positional arg), not the missing contract — so it was reverted and a single justified suppression added (verified false positive;config-modal.tsreads it in production). - Plan-completeness gaps caught at the end and fixed: the
skill-prompt-sanitizer.tsSkillPermissionCheckerdoc comment still namedPermissionSession(which no longer hascheckPermission) — corrected toPermissionResolver. - Marked Steps 5 (
#338) and 7 (#340)✓ completein the roadmap — both were CLOSED but unmarked (the user flagged#338). Step 8 (#341) stays unmarked until/ship-issueper convention. - Reviewer's one WARN is informational:
PermissionResolver.checkPermissionis intentionally dual-role (ruleset-injectingresolvevs. rawSkillPermissionCheckerpre-filter) — deliberate design carried over from#340, no change needed. Edit-tool friction: the Unicode box-drawing comment banners inpermission-session.tsand the architecture doc twice defeatedoldTextmatching (compounded bypi-autoformatreflow); fell back to a Python slice for the two block removals. Re-reading after autoformat resolved the rest.
Stage: Final Retrospective (2026-06-07T20:47:46Z)
Session summary
Shipped #341 across Planning (claude-opus-4-8), TDD (claude-sonnet-4-6, with an opus escalation for one design question), and Ship (deepseek-v4-flash): PermissionSession became a state/lifecycle owner, the recorder role moved to SessionRules, the two non-gate handlers were rewired to a concrete PermissionResolver, and the three fig-leaf handler interfaces were deleted.
Released as pi-permission-system-v10.5.1; behavior-preserving; net test delta −5; pre-completion reviewer returned PASS.
The defining moment was a user "step back" question that converted a fallow-suppression band-aid into the truthful implements ToolCallGateInputs contract declaration.
Observations
What went well
- The lift-and-shift TDD order held the suite green at every commit, and the planning prediction "the 104
makeHandlercall sites stay put" was correct — only three handler-test assertions needed edits becausemakeHandlerpreserved its override-bag surface. - The model ladder matched task weight at every stage; notably the
sonnet→opusswitch coincided with the user's design question and gave the structural reasoning (implementsvs. suppress) the right model. - Incremental verification was disciplined:
pnpm run checkplus a targetedvitest runafter each step, the full suite before each commit, andfallowat the end-of-TDD gate. - The user's "step back" redirect — a question, not a correction — is the standout: it reframed a band-aid into a truthful design fix (
implements ToolCallGateInputs) and surfaced a generalizablefallowinsight worth promoting.
What caused friction (agent side)
premature-convergence/wrong-abstraction— whenfallowflagged fourPermissionSessionmembers after Step 5 removed the lastimplements, the agent (onsonnet) reached for fourfallow-ignoresuppressions without first asking "why isfallowflagging these?". The user caught it ("Take a step back. Why are we having to tell fallow these methods are used?"). Impact: ~4 tool calls of suppression work reverted; the root-cause investigation it triggered would have been needed regardless, so net rework was small but the design-quality delta was large (truthful contract vs. four band-aids). User-caught.missing-context— thefallowskill was not loaded during TDD (the same gap the#340retro noted); loading it is the natural first move whenfallowflags findings, and its absence reinforced the suppress-first reflex. The skill did not yet document theimplements-liveness behavior anyway — hence the proposal below. Impact: contributed to the premature-suppression reflex; recurring across#340and#341.- Edit-tool friction (recurring, ~6 occurrences) — Unicode box-drawing banner comments (
// ── … ──) inpermission-session.ts,handler-fixtures.ts, andarchitecture.mddefeatedoldTextmatching (variable-length dash runs compounded bypi-autoformatreflow); the reliable workaround was a Python.find()slice on a short substring. Impact: added friction, no rework — each recovered within 1–2 calls.
What caused friction (user side)
- Several
Continue.nudges during multi-file TDD steps where the agent paused after a tool batch. Mechanical oversight rather than strategic input; the agent was making steady progress. Opportunity: batch the remaining edits of a single step more aggressively so a multi-file step does not stall waiting for a nudge (same observation as the#340retro — recurring). - The
#338✓ completegap: the user had to point out that a prior CLOSED roadmap step was never marked complete inarchitecture.md. Opportunity: the doc-update step could prompt re-checking sibling roadmap steps' completion marks, not just the current issue's.
Diagnostic details
- Model-performance correlation — clean, no mismatches.
Planning ran on
claude-opus-4-8(design ambiguity +ask_usergate), TDD onclaude-sonnet-4-6(implementation), the fallow design question escalatedsonnet→opus(correct — structural-design judgment), Ship ondeepseek-v4-flash(mechanical git/CI/release), and thepre-completion-reviewersubagent returned a thorough PASS. The escalation landing exactly at the judgment-heavy question is the model ladder working as intended. - Escalation-delay tracking — no long rabbit-hole.
The suppression episode was ~4 tool calls before the user redirected; the post-redirect investigation (root cause →
implementsfix → named-interface attempt → revert → one justified suppression) made steady forward progress rather than repeating a failing approach. - Unused-tool detection — the
fallowskill was available but not loaded during TDD; this is the second consecutive issue (#340,#341) where it would have been the right first reach when dead-code findings appeared. - Feedback-loop gap analysis — verification was incremental and effective;
fallowcorrectly ran at the end-of-TDD gate per/tdd-plan. The gap was design-foresight (not anticipating that removing the lastimplementswould blindfallowto structurally-consumed members), not a missing verification run.
Changes made
.pi/skills/fallow/SKILL.md— added "Key gotchas" item 6:fallowkeys class-member liveness offimplementsclauses, so a structurally-consumed member reads as dead once the lastimplementsis removed; prefer re-declaring the contract over suppressing..pi/prompts/tdd-plan.md— reframed the end-of-TDDfallowstep to load thefallowskill and prefer declaring a real contract / removing dead exports over suppressing (suppress only verified false positives), replacing the prior "add suppressions for false positives" wording that nudged toward the suppress-first reflex.- Recorded (no rule change): the Edit-tool
// ── … ──banner-matching friction (covered by existing minimal-oldTextguidance), the#338✓ completegap (single-occurrence historical hygiene), and the recurringContinue.-nudge batching observation (judged too marginal for a crisp rule in the#340retro).