--- issue: 557 issue_title: "pi-permission-system: serving is resolution — rebuild processInbox on evaluate() + the serving session's Authorizer" --- # Retro: #557 — serving is resolution — rebuild `processInbox` on `evaluate()` + the serving session's `Authorizer` ## Stage: Planning (2026-07-08T00:00:00Z) ### Session summary Planned Phase 9 Step 3 across two session branches: an exploration branch mapped the resolution pipeline and uncovered that the issue's literal "escalate to the serving `Authorizer`" would silently re-degrade the forwarded `permissions:ui_prompt` broadcast (the #292 D3/D4 "not degraded" hardening, a documented public contract in `docs/cross-extension-api.md`); this branch stepped back to the phase level, amended the Phase 9 roadmap (commit `21472cf9`: Step 3 fidelity invariant, resolved-direction-1 provenance sentence, Step 4 target reworded to the post-Step-3 topology), and committed the plan (`0557-serving-is-resolution.md`, commit `14cdeea5`). Filed follow-up #565 for post-ship validation of the recorded decisions. ### Observations - **Phase-planning gap class**: the roadmap recorded the policy invariant ("parent rules govern children") but not the presentation invariant ("observers see forwarded prompts undegraded") — a step that reroutes an emission path must be checked against `docs/cross-extension-api.md` contracts, not just `src/` behavior. The pre-completion reviewer only catches invariants the roadmap documents, so the amendment (not just the plan) was the right fix. - **Decide-gate outcomes** (operator-confirmed via `ask_user`): (1) fidelity becomes a documented Step 3 invariant with the threaded design — provenance as data on `PromptPermissionDetails`, rendered by `LocalUserAuthorizer`, the single emit site; (2) amendment scope = Step 3 + resolved direction 1 + Step 4 target; (3) serving evaluates the **base** ruleset (`agentName` undefined; requester agent name is display-only). - **"Threading" smell concern resolved**: operator asked for reassurance that threading isn't a compromise. Verified against the taxonomy — not tramp data (every hop reads/relays it), not a control flag (rendered, not branched on), and it echoes the architecture doc's own principal-identity requirement; the rejected alternatives (server-side emission + emit-suppressed authorize; per-request decorator) each re-create a smell Phase 9 kills. - **Wiring findings**: `ForwardingManager.start` already gates polling on `hasUI && !isSubagent`, so the server's internal `ctx.hasUI` guard is removable dead defense; `index.ts` needs a construction reorder (`prompter` → `authorizerSelection` → `resolver` → `servingPolicy` → `requestServer`), with the `session.getPathNormalizer()` read deferred via the existing logger-`notify`-sink precedent. - **Accepted behavior shifts** (named for the `feat:` commit body): explicit `deny` now wins under yolo on the serving path; legacy field-less requests prompt instead of yolo-approving; policy-decided requests emit no `permissions:ui_prompt`. - Deferred: single-surface re-resolution fidelity, base-agent-scope revisit, and notification-consumer verification all live in #565. ## Stage: Implementation — TDD (2026-07-09T12:45:00Z) ### Session summary Implemented all four planned TDD cycles plus one pre-completion fix, landing Phase 9 Step 3. Step 1 (`refactor`) added `buildUiPrompt` (folding `buildDirectUiPrompt`/`buildForwardedUiPrompt`), extended `PromptPermissionDetails` with `ForwardedAskProvenance` + `surface`/`value`, and made `LocalUserAuthorizer` render forwarded provenance (non-degraded broadcast + `(Subagent)` title). Step 2 (`feat`) rebuilt `ForwardedRequestServer` on the `ServingPolicy` + `AskEscalator` seams with the one-hop canary, rewired `index.ts` (resolver moved up, `servingPolicy` adapter with deferred `getPathNormalizer`), and removed the old builders. Steps 3–4 (`docs`) added ADR-0005 and marked the roadmap step complete. Test count went 2280 → 2290 (+10, net of removed `buildDirectUiPrompt`/`buildForwardedUiPrompt` cases and added `buildUiPrompt`/server/authorizer cases); all 116 files green. ### Observations - **Pre-completion reviewer: PASS** (after one FAIL→fix cycle). First pass returned **FAIL** on the sole quantitative target: `processSingleForwardedRequest` landed at 74 lines vs. the plan/issue `< 60` commitment. Operator chose to fix; extracted `recordForwardedDecision` (the "respond" half symmetric to the existing `resolveDecision` "decide" half) — a genuine SRP split, not number-forcing — dropping the method to 43 lines. Re-dispatch returned PASS. Lesson: the deterministic gate (`fallow dead-code`) does not catch a `fallow health` line-count/complexity target, so a plan's LOC commitment needs an explicit `fallow health` check before declaring done. - **Plan-grep miss (minor deviation)**: the plan's Module-Level Changes listed only `local-user-authorizer.ts` as a `buildDirectUiPrompt` consumer, but `approval-escalator.ts` (`ParentAuthorizer.authorize`) also imported it to compute the forwarded request's display projection. Switched it to `buildUiPrompt` in Step 2 — behavior-identical (the old builder was a thin wrapper), and correct for the multi-hop-ready design (it now honors an explicit `surface`/`value`/`forwarding` override). The reviewer confirmed benign; the pre-existing `approval-escalator.test.ts` stayed green unchanged. - **Provenance types identical by design**: `ForwardedAskProvenance` (declared in `permission-prompter.ts` to keep that layer free of an events import) is structurally identical to the event's `ForwardedPromptContext`, so `PromptPermissionDetails` flows straight into `buildUiPrompt` with no copying — as the plan intended. - **Behavior shifts shipped as documented**: parent `allow`/`deny` now govern children's escalations; explicit `deny` wins under yolo on the serving path; field-less legacy requests escalate instead of yolo-auto-approving; policy-decided requests emit no `permissions:ui_prompt` (one sentence added to `cross-extension-api.md`, the "not degraded" guarantee preserved verbatim). - All cross-step invariants held (reviewer-confirmed), notably the #292 non-degraded broadcast (re-pinned by composing `LocalUserAuthorizer` + server-details tests) and the escalate-requires-activated-selection timing invariant added to the plan this session. ## Stage: Final Retrospective (2026-07-09T16:59:56Z) ### Session summary Shipped Phase 9 Step 3 end-to-end: folded two plan tightenings (escalate-timing invariant, drain serialization) into the plan, executed all four TDD cycles plus a pre-completion FAIL→fix, then pushed, closed #557, and merged release-please PR #566 (`pi-permission-system` v20.2.0). The run was clean apart from one substantive rework (a plan LOC target missed until the reviewer measured it) and three trivial self-corrected tool slips. ### Observations #### What went well - **The pre-completion reviewer earned its keep on a target the deterministic gates cannot see.** Every green check passed (`check`, `lint`, `test` 2290, `fallow dead-code`), yet the plan's `processSingleForwardedRequest < 60 lines` commitment was unmet at 74 lines. Only the reviewer (on `anthropic/claude-sonnet-5`) measured it and returned FAIL — a genuine value-add, since `fallow dead-code` does not measure LOC/complexity. - **The planning-stage foresight paid off at implementation.** Because the prior session amended the Phase 9 roadmap to record the #292 non-degraded-broadcast invariant as a documented Step 3 outcome, the reviewer could verify it and the threaded-provenance design preserved it with a green suite — the presentation invariant did not silently regress. - **Clean release-please UNSTABLE handling.** The `release_pr_merge` refusal was a genuinely `IN_PROGRESS` CI check (not the empty-rollup `GITHUB_TOKEN` case), so the ship flow waited for CI (`ci_watch` on the PR head SHA) and retried, rather than force-merging — the protocol's distinction held. #### What caused friction (agent side) - `missing-context` — the plan committed to `processSingleForwardedRequest < 60 lines`, but no TDD-loop step measured it; `fallow health` never ran during implementation (only `fallow dead-code`, which does not measure LOC). The miss surfaced only as a pre-completion FAIL. Impact: one extra `refactor:` commit (`4b103618`, extracting `recordForwardedDecision`) and a full FAIL→fix→re-dispatch reviewer cycle (~350s of reviewer time across two dispatches). The fix itself was a genuine decide/respond SRP split, not number-forcing. - `instruction-violation` (self-identified) — a defensive `cd /Users/chris/.../pi-permission-system 2>/dev/null; cd ...` prefix on a vitest command hit the `external_directory` permission gate (the first path does not exist at that level) and was denied; AGENTS.md bans prefixing a command with `cd` into the working directory. Impact: one wasted tool call, immediately re-run without the `cd`. - `instruction-violation` (self-identified) — during the ship UNSTABLE-wait, passed the literal string `HEAD` to `ci_find`'s `expected_sha` instead of a resolved 40-char SHA; the ship prompt says pass the exact SHA, never a value from memory. Impact: one ~125s non-blocking `ci_find` timeout, recovered via `gh pr view --json headRefOid` + `ci_watch`. - `other` (self-identified) — an `Edit` batch on `index.ts` carried a stray `newText_unused` key and was rejected wholesale; re-issued correctly. Impact: one wasted tool call. #### What caused friction (user side) - None material. The operator's one substantive decision (fix vs. skip the 74-line FAIL) was answered promptly and correctly; the earlier "are we ready?" check surfaced two real plan tightenings before implementation, which is the intended pre-flight. ### Diagnostic details - **Model-performance correlation** — the only subagent dispatches were the two `pre-completion-reviewer` runs on `anthropic/claude-sonnet-5`; judgment-heavy review (acceptance-criteria verification, precise LOC measurement, invariant cross-checking) is well-matched to that model, and it caught the quantitative miss the deterministic gates missed. No mismatch. - **Escalation-delay tracking** — no `rabbit-hole` points; no error ran longer than one corrective tool call. The 74-line FAIL resolved in a single extraction. - **Unused-tool detection** — `pnpm fallow health` was the available-but-unused tool: running it on the touched file before the reviewer dispatch would have caught the LOC miss locally. - **Feedback-loop gap analysis** — incremental verification was otherwise strong (`pnpm run check` after each shared-type change, per-file red/green vitest, full suite before commits). The single gap was the `fallow health` LOC/complexity check, which never ran in the TDD loop — the direct cause of the FAIL→fix cycle. ### Changes made 1. Appended this Final Retrospective stage entry to `packages/pi-permission-system/docs/retro/0557-serving-is-resolution.md`. 2. Proposed adding a `pnpm fallow health` quantitative-target check to `.pi/prompts/tdd-plan.md` ("After the last TDD step"); operator declined — no prompt change made. The observation stays recorded here as the session's primary friction finding.