10 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 322 | Extract a DecisionReporter for permission gate review-log and decision events |
Retro: #322 — Extract a DecisionReporter for permission gate review-log and decision events
Stage: Planning (2026-06-03T00:47:01Z)
Session summary
Planned the extraction of a DecisionReporter interface + GateDecisionReporter class that owns the SessionLogger and the event bus, removing the writeReviewLog/emitDecision closures and the Law-of-Demeter reach-through to session.logger.review from PermissionGateHandler.
Confirmed #319's PermissionResolver work has already landed in src/, so the prerequisite is satisfied despite the issue still being open.
Produced a four-step plan (new module + test, atomic runner wiring, handleInput adoption, architecture doc) and committed it.
Observations
- Key design fork surfaced via
ask_user: how the reporter reachesrunGateCheck. Rejected a 5th positional parameter in favor of carryingreporter: DecisionReporterinside theGateRunnerDepsbag. The user's read — thatrunGateCheckis "hiding a class … instantiated with what they need to act on the extemporaneous data" — confirms the #323 trajectory: the bag's stable role collaborators becomeGateRunnerconstructor fields whiledescriptor/agentName/toolCallIdstay per-call arguments. The bag is the deliberate intermediate, not the destination. - Second
ask_userdecision: routehandleInputthrough the same constructor-built reporter (chosen), removing a second reach-through andunbound-methoddisable. Building the reporter once in the constructor (not perhandleToolCall) is the faithful reading of "build it once" and enables thehandleInputreuse. - Test churn is narrow: the four handler integration test files need no changes because they assert through the real event bus (
getDecisionEventsonevents.emit) and thesession.logger.reviewmock — both routed identically by the reporter. Churn is concentrated ingate-fixtures.tsmakeRunnerDepsand ~13runner.test.tsassertion sites (deps.reporter.*), plus a newdecision-reporter.test.ts. - Mid-plan correction from the user: the architecture doc's gate-runner decomposition chain (row 6, Step 6 Outcome, Track C summary,
S6Mermaid node) stops at #323 and omits #325 — the phase capstone that retypesPermissionGateHandleragainst the role interfaces and drops theas unknown as PermissionSessioncasts. Step 4 of the plan now threads #325 into every link in that chain and adds the missing[#325]link reference, even though #325's residual-cluster decomposition is still nebulous. step 2is the single mandated atomic commit: removing the two inline members fromGateRunnerDepsbreaks the descriptor, runner, handler, fixture, and runner test at the type level simultaneously, so they move together.- Scope held tight:
emitDecisionEventand thepermissions:decisionchannel are untouched; the reporter wraps the existing primitive. No public export is removed or renamed (member swap on an exported interface only).
Stage: Implementation — TDD (2026-06-03T01:19:36Z)
Session summary
Executed all four TDD steps: added GateDecisionReporter (7 new tests), wired the reporter atomically into GateRunnerDeps + runner + handler + fixtures (19 runner tests reshaped), routed handleInput through the reporter (25 handler tests stayed green), and updated architecture.md with a fully flattened 11-step Phase 3 roadmap including new steps 8–9 for #323 and #325.
Test count: 1763 → 1770 (+7).
Pre-completion reviewer verdict: PASS.
Observations
- Step 2 required one course-correction: the initial edit dropped
emitDecisionEventfrom the import prematurely, breaking the 14handleInputtests that still called it directly. Restored the import to keep step 2 self-contained; step 3 removed it cleanly. - Step 2 also required a second correction: removing
PermissionDecisionEventfromdescriptor.ts's imports when replacing the inlineemitDecisionmember madeGateBypass.decisionimplicitlyany, causing@typescript-eslint/no-unsafe-argumentat commit time. Added the import back; lesson: when removing a named interface member that references an imported type, grep for other uses of that type in the same file before dropping the import. - Two WARN findings from the pre-completion reviewer, both fixed before shipping:
private readonly events: PermissionEventBuson the handler class was vestigial after the reporter extraction (only used in the constructor to build the reporter); Biome flagged it asnoUnusedPrivateClassMembers. Fixed by droppingprivate readonlyto make it a plain constructor parameter.makeReporterwas missing from thegate-fixtures.tsentry inpackage-pi-permission-systemSKILL.md. Added alongsidemakeRunnerDeps/makeResolver.
- Architecture doc update expanded beyond the plan: the previous S6 Mermaid node encoded the entire four-step chain in a single label (a holdover from the original coarse planning). Flattened to 11 discrete steps (S6–S11) per the user's direction, adding placeholder steps for #323 and #325 alongside the renumbering of the composition-root (#320) and test-fixture (#321) steps. Pre-completion reviewer: PASS.
Stage: Final Retrospective (2026-06-03T01:23:15Z)
Session summary
One continuous session carried #322 from planning through TDD to ship-ready: an 8-commit arc that extracted a DecisionReporter role (interface + GateDecisionReporter), rewired the gate runner and handleInput through it, and flattened the Phase 3 roadmap in architecture.md to 11 discrete steps.
Test count 1763 → 1770 (+7); all gates green; pre-completion reviewer PASS after two WARN fixes.
The dominant theme across stages was local-minimal edits that missed the broader structural picture — both required user redirection.
Observations
What went well
- The plan's narrow test-churn prediction held exactly: the four handler integration test files (
tool-call,tool-call-events,input,input-events) needed zero changes because they assert through the real event bus (getDecisionEventsonevents.emit) and thesession.logger.reviewmock — both routed identically by the reporter. Accurate test-impact analysis at plan time meant the TDD churn landed precisely where predicted (gate-fixtures.ts,runner.test.ts, one new file). - The pre-completion reviewer earned its keep: it flagged the vestigial
private readonly eventsfield that Biome reports only as a warning (exit 0), so the pre-commit hook let it through — the reviewer caught what the deterministic gate did not.
What caused friction (agent side)
missing-context— the planning-stage architecture-doc update referenced only #319/#322/#323 in the gate-runner decomposition chain and omitted #325, the phase capstone that depends on #322. #325 was not named in #322's issue body, so following only the issue's own references missed it; a forward search for dependents (gh issue list --search "#322") would have surfaced it. Impact: user-caught; plan amended (4 edits) during planning, no code rework.premature-convergence— asked to thread #325 into the roadmap, the first attempt did the minimal in-place edit: it left the compressedS6Mermaid node encoding the whole four-issue chain in one label and added a redundantS7, producing an inconsistent hybrid. The user redirected ("the cleanest approach is to flatten and renumber the steps, no? … it saves us in the end"), and the chain was re-expanded to 11 flat one-issue-per-step nodes. Impact: user-caught; one extra round-trip plus a redo of the Mermaid graph, step list, and Tracks table.missing-context— TDD step 2 dropped two still-needed imports prematurely:emitDecisionEvent(still used byhandleInput, broke 14 tests) andPermissionDecisionEvent(still referenced byGateBypass.decisionindescriptor.ts, made it implicitlyanyand tripped@typescript-eslint/no-unsafe-argument). Impact: self-caught — the affected-file test run and the pre-commit eslint hook each caught one within 1–2 tool calls; near-zero rework.
What caused friction (user side)
- Both user redirects (#325 omission, hybrid flatten) were structural-breadth catches the user had to make twice in the same session. Opportunity: a forward-dependency check and a "keep the roadmap step list flat" convention, encoded once, would let the user stay in strategic-review mode rather than mechanically catching the same class of local-minimal slip.
Diagnostic details
- Model-performance correlation — the parent session bounced across
anthropic/claude-opus-4-8,anthropic/claude-sonnet-4-6, andopencode-go/deepseek-v4-flash. Both structural-breadth slips were user-caught rather than self-caught; the timeline cannot be confidently pinned to a specific model from the session data, but the pattern is consistent with a lighter model running during the architecture-doc edits. Thepre-completion-reviewersubagent did judgment-heavy work (261s, 36 tool uses) and returned two accurate WARNs — appropriately capable for the task. - Escalation-delay tracking — no rabbit-holes; the two premature-import errors were each resolved in 1–2 tool calls. No sequence exceeded 5 consecutive calls on one error.
- Unused-tool detection —
gh issue list --search "#322"(or--search "depends 322") was available and never run during planning; it would have surfaced #325 before the user did. No subagent was needed. - Feedback-loop gap analysis — verification was incremental and healthy: each TDD step ran its affected test file red→green, then the full suite +
check+lint+fallow dead-coderan after the last step, with pre-commit hooks catching the eslint slip at commit time. No end-only-verification gap.
Changes made
- Added a two-sentence rule to
.pi/skills/package-pi-permission-system/SKILL.md(right after thedocs/plans/line): thearchitecture.mdphase roadmap is a flat one-issue-per-step list (never a chain inside one node label), and a plan touching it must enumerate the whole phase via a dependent search (gh issue list --search "#N"), not just the issues the current one references.