Files
my-pi/pi-permission-system/docs/retro/0322-extract-decision-reporter.md
T

102 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 322
issue_title: "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 reaches `runGateCheck`.
Rejected a 5th positional parameter in favor of carrying `reporter: DecisionReporter` inside the `GateRunnerDeps` bag.
The user's read — that `runGateCheck` is "hiding a class … instantiated with what they need to act on the extemporaneous data" — confirms the #323 trajectory: the bag's stable role collaborators become `GateRunner` constructor fields while `descriptor`/`agentName`/`toolCallId` stay per-call arguments.
The bag is the deliberate intermediate, not the destination.
- Second `ask_user` decision: route `handleInput` through the same constructor-built reporter (chosen), removing a second reach-through and `unbound-method` disable.
Building the reporter once in the constructor (not per `handleToolCall`) is the faithful reading of "build it once" and enables the `handleInput` reuse.
- Test churn is narrow: the four handler integration test files need **no** changes because they assert through the real event bus (`getDecisionEvents` on `events.emit`) and the `session.logger.review` mock — both routed identically by the reporter.
Churn is concentrated in `gate-fixtures.ts` `makeRunnerDeps` and ~13 `runner.test.ts` assertion sites (`deps.reporter.*`), plus a new `decision-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, `S6` Mermaid node) stops at #323 and omits #325 — the phase capstone that retypes `PermissionGateHandler` against the role interfaces and drops the `as unknown as PermissionSession` casts.
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 2` is the single mandated atomic commit: removing the two inline members from `GateRunnerDeps` breaks the descriptor, runner, handler, fixture, and runner test at the type level simultaneously, so they move together.
- Scope held tight: `emitDecisionEvent` and the `permissions:decision` channel 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 89 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 `emitDecisionEvent` from the import prematurely, breaking the 14 `handleInput` tests 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 `PermissionDecisionEvent` from `descriptor.ts`'s imports when replacing the inline `emitDecision` member made `GateBypass.decision` implicitly `any`, causing `@typescript-eslint/no-unsafe-argument` at 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:
1. `private readonly events: PermissionEventBus` on the handler class was vestigial after the reporter extraction (only used in the constructor to build the reporter); Biome flagged it as `noUnusedPrivateClassMembers`.
Fixed by dropping `private readonly` to make it a plain constructor parameter.
2. `makeReporter` was missing from the `gate-fixtures.ts` entry in `package-pi-permission-system` SKILL.md.
Added alongside `makeRunnerDeps`/`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 (S6S11) 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 (`getDecisionEvents` on `events.emit`) and the `session.logger.review` mock — 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 events` field 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 compressed `S6` Mermaid node encoding the whole four-issue chain in one label and added a redundant `S7`, 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 by `handleInput`, broke 14 tests) and `PermissionDecisionEvent` (still referenced by `GateBypass.decision` in `descriptor.ts`, made it implicitly `any` and tripped `@typescript-eslint/no-unsafe-argument`).
Impact: self-caught — the affected-file test run and the pre-commit eslint hook each caught one within 12 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`, and `opencode-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.
The `pre-completion-reviewer` subagent 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 12 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-code` ran after the last step, with pre-commit hooks catching the eslint slip at commit time.
No end-only-verification gap.
### Changes made
1. Added a two-sentence rule to `.pi/skills/package-pi-permission-system/SKILL.md` (right after the `docs/plans/` line): the `architecture.md` phase 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.