9.2 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 598 | pi-permission-system: Authorizer chain infrastructure (allow/deny/defer verdicts) |
Retro: #598 — Authorizer chain infrastructure (allow/deny/defer verdicts)
Stage: Planning (2026-07-18T00:00:00Z)
Session summary
Planned Phase 12 Step 4 (Track B, batch "authorizer-chain" head): reshape the live-authority layer into a Chain of Responsibility per ADR 0007, with zero registered links so behavior is identical to today.
Produced docs/plans/0598-authorizer-chain-infrastructure.md with three commits — an atomic interface reshape + composeAuthorizerChain, a wiring step routing AuthorizerSelection.activate through the empty chain, and a doc-completion step.
Observations
- Terminal return type is the key reconciliation.
ADR 0007 sketches a minimal
TerminalVerdict(allow | denykind union), but the real terminals return the richPermissionPromptDecision(session-scope states,confirmationUnavailable,denialReason). The "behavior identical" constraint forces keepingPermissionPromptDecisionas the terminal's return; the ADR sketch is illustrative ("the essentials follow"). Recorded as a Non-Goal so Step 5 doesn't re-litigate it. - Naming decision surfaced via
ask_user. The ADR reassigns the nameAuthorizerto the non-terminal link and introducesTerminalAuthorizerfor the terminal, but todayAuthorizeris the terminal interface (3 concrete classes implement it). Operator chose the ADR-faithful rename over an additiveAuthorizerLink, so Steps 5/6 inherit ADR vocabulary directly. - Empty-links identity is a behavioral invariant, not an optimization.
composeAuthorizerChain([], terminal)must return the terminal instance soauthorizer-selection.test.ts'sexpect.any(LocalUserAuthorizer)still holds. Called out in Design Overview + Invariants at risk. PermissionQuerydeferred to Step 5. ADR 0007 §3 ties the injected query to the registration seam; a Step-4 link signature takes onlyPromptPermissionDetails. Step 5 will widen the linkauthorizesignature — noted as an Open Question so it's not read as an oversight.- Release: mid-batch defer.
Step 4 is the batch "authorizer-chain" head (tail = Step 5, #599); the
docs:step-completion commit lands in the pending release-please PR but must not be merged until Step 5 ships.refactor:/test:commits arehidden:and don't cut a release. - No follow-up issues filed — Steps 5 (#599) and 6 (#600) already exist in the roadmap.
Stage: Implementation — TDD (2026-07-19T10:05:00Z)
Session summary
Implemented all three TDD cycles for Phase 12 Step 4: reshaped the live-authority layer into a Chain of Responsibility per ADR 0007 with zero registered links, so behavior is byte-identical.
Added AuthorizerVerdict + the non-terminal Authorizer / terminal TerminalAuthorizer split, the new composeAuthorizerChain, routed AuthorizerSelection.activate through the (empty) chain, and marked the roadmap step complete.
Test count 2499 → 2506 (+7 composeAuthorizerChain unit tests); full suite, check, root lint, and fallow dead-code all green.
Observations
- No preparatory tidying warranted.
The
tidy-first-assessorfound the interface split inherently atomic (repurposing the exportedAuthorizertype breaks every implementer/consumer at compile time simultaneously) with no length/coupling/duplication friction in the target files — proceeded directly to the cycle. - The plan held exactly.
All three commits landed as planned (
refactor:/refactor:/docs:); every file in Module-Level Changes was touched and the two behavior pins (authorizer.test.ts,authorizer-selection.test.ts) stayed unchanged (zero-line diffs). No deviations. - Empty-links identity verified two ways.
composeAuthorizerChain([], terminal)returns the terminal instance, pinned both by the unchangedauthorizer-selection.test.ts(expect.any(LocalUserAuthorizer)) and a dedicatedtoBe(terminal)unit test. - One tool-friction note: an
Edittoarchitecture.mdwas first denied by theexternal_directorygate because I used a wrong path (.../pi-permission-system/docs/...instead of.../pi-packages/packages/pi-permission-system/docs/...) — corrected on retry. (A live instance of exactly the typo-path class ADR 0007's future model judge targets.) - Pre-completion reviewer: PASS — ready for
/ship-issue. Reviewer reconfirmed the mid-batch defer: thedocs:completion commit sits in the pending release-please PR unmerged until Step 5 (#599) ships.
Stage: Final Retrospective (2026-07-19T15:17:13Z)
Session summary
Shipped Phase 12 Step 4 across a single-process planning → TDD → ship sequence: the live-authority layer is now a Chain of Responsibility (ADR 0007) with zero registered links, so behavior is byte-identical. Four commits landed exactly as planned, CI passed, #598 closed, and the release was correctly deferred (batch "authorizer-chain" head; tail = Step 5, #599). An unusually clean run — the plan held with zero deviations and the pre-completion reviewer returned PASS on the first pass.
Observations
What went well
- The plan held with zero deviations.
Every file in Module-Level Changes was touched, the two behavior pins stayed at zero-line diffs, and the three TDD commits matched their planned messages and types.
Front-loading the design reconciliation (terminal keeps
PermissionPromptDecision; ADR-faithful rename viaask_user) into planning left nothing to re-decide during implementation. tidy-first-assessorcorrectly declined (novel). On this dispatch the assessor recommended no preparatory work — it recognized the interface split as inherently atomic (repurposing the exportedAuthorizertype breaks every implementer at compile time simultaneously) with no length/coupling/duplication friction, and its Rejected-as-scope-creep list stayed inside the change's own files. This is the skill's "first-live-use checkpoint" behaving as intended: a clean decline rather than manufactured busywork.- The empty-links identity was pinned two ways.
composeAuthorizerChain([], terminal)returning the terminal instance is guarded by both the unchangedauthorizer-selection.test.ts(expect.any(LocalUserAuthorizer)) and a dedicatedtoBe(terminal)unit test — stronger than prose-only, and the pre-completion reviewer called this out. - Incremental verification cadence.
pnpm run checkran right after the shared-interface change (Step 1), affected-file tests ran per step, and the full suite + rootlint+fallow dead-coderan once at the end — no end-of-session verification pile-up.
What caused friction (agent side)
other(Edit-tool mechanics) — the firstEdittodenying-authorizer.test.tsused anoldText(const authorizer: Authorizer = new DenyingAuthorizer();) that matched two occurrences and was rejected as non-unique. Impact: one extra retry with wider context; no rework, no wrong edit applied (the batch is atomic, so nothing landed on the miss).other(path typo, self-identified) — anEdittoarchitecture.mdused a wrong absolute path (.../pi-permission-system/docs/..., dropping thepi-packages/packages/segment) and was denied by theexternal_directorygate. Impact: one retry with the corrected path; no rework. The gate fail-safe caught it immediately — and it is a live instance of exactly the typo-path class ADR 0007's future model judge is designed to auto-deny with a teaching reason.
What caused friction (user side)
- None.
The single
ask_userat planning (ADR-faithful rename vs. additiveAuthorizerLink) and the ship-time release-defer confirmation were both well-scoped strategic decisions, answered once each with no back-and-forth.
Diagnostic details
- Model-performance correlation — the main planning/TDD/ship work ran on
claude-opus-4-8(appropriate for the ADR-reconciliation judgment), with a mid-session switch toclaude-sonnet-5and back. Both read-only subagents (tidy-first-assessor,pre-completion-reviewer) ran onclaude-sonnet-5per their frontmatter — fitting for assessment/review. No mismatch: judgment-heavy work stayed on the strong model, mechanical read-only sweeps on the cheaper one. - Escalation-delay tracking — no
rabbit-holefriction; both friction points resolved on the first retry (well under the 5-consecutive-call threshold). - Unused-tool detection — not applicable; no
missing-contextorrabbit-holefriction.colgrep/grep/Readwere used appropriately during planning exploration. - Feedback-loop gap analysis — no gap; verification was incremental (see "Incremental verification cadence" above), matching the AGENTS.md rule to run
pnpm run checkimmediately after a shared-interface change.
Changes made
- Wrote this Final Retrospective stage entry in
packages/pi-permission-system/docs/retro/0598-authorizer-chain-infrastructure.md. NoAGENTS.mdor prompt changes — both friction points were one-off, self-corrected tool mechanics with zero rework, so no rule change was justified (operator confirmed "retro file only").