11 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 557 | 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.mdcontracts, not justsrc/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 onPromptPermissionDetails, rendered byLocalUserAuthorizer, the single emit site; (2) amendment scope = Step 3 + resolved direction 1 + Step 4 target; (3) serving evaluates the base ruleset (agentNameundefined; 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.startalready gates polling onhasUI && !isSubagent, so the server's internalctx.hasUIguard is removable dead defense;index.tsneeds a construction reorder (prompter→authorizerSelection→resolver→servingPolicy→requestServer), with thesession.getPathNormalizer()read deferred via the existing logger-notify-sink precedent. - Accepted behavior shifts (named for the
feat:commit body): explicitdenynow wins under yolo on the serving path; legacy field-less requests prompt instead of yolo-approving; policy-decided requests emit nopermissions: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:
processSingleForwardedRequestlanded at 74 lines vs. the plan/issue< 60commitment. Operator chose to fix; extractedrecordForwardedDecision(the "respond" half symmetric to the existingresolveDecision"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 afallow healthline-count/complexity target, so a plan's LOC commitment needs an explicitfallow healthcheck before declaring done. - Plan-grep miss (minor deviation): the plan's Module-Level Changes listed only
local-user-authorizer.tsas abuildDirectUiPromptconsumer, butapproval-escalator.ts(ParentAuthorizer.authorize) also imported it to compute the forwarded request's display projection. Switched it tobuildUiPromptin Step 2 — behavior-identical (the old builder was a thin wrapper), and correct for the multi-hop-ready design (it now honors an explicitsurface/value/forwardingoverride). The reviewer confirmed benign; the pre-existingapproval-escalator.test.tsstayed green unchanged. - Provenance types identical by design:
ForwardedAskProvenance(declared inpermission-prompter.tsto keep that layer free of an events import) is structurally identical to the event'sForwardedPromptContext, soPromptPermissionDetailsflows straight intobuildUiPromptwith no copying — as the plan intended. - Behavior shifts shipped as documented: parent
allow/denynow govern children's escalations; explicitdenywins under yolo on the serving path; field-less legacy requests escalate instead of yolo-auto-approving; policy-decided requests emit nopermissions:ui_prompt(one sentence added tocross-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,test2290,fallow dead-code), yet the plan'sprocessSingleForwardedRequest < 60 linescommitment was unmet at 74 lines. Only the reviewer (onanthropic/claude-sonnet-5) measured it and returned FAIL — a genuine value-add, sincefallow dead-codedoes 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_mergerefusal was a genuinelyIN_PROGRESSCI check (not the empty-rollupGITHUB_TOKENcase), so the ship flow waited for CI (ci_watchon 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 toprocessSingleForwardedRequest < 60 lines, but no TDD-loop step measured it;fallow healthnever ran during implementation (onlyfallow dead-code, which does not measure LOC). The miss surfaced only as a pre-completion FAIL. Impact: one extrarefactor:commit (4b103618, extractingrecordForwardedDecision) 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 defensivecd /Users/chris/.../pi-permission-system 2>/dev/null; cd ...prefix on a vitest command hit theexternal_directorypermission gate (the first path does not exist at that level) and was denied; AGENTS.md bans prefixing a command withcdinto the working directory. Impact: one wasted tool call, immediately re-run without thecd.instruction-violation(self-identified) — during the ship UNSTABLE-wait, passed the literal stringHEADtoci_find'sexpected_shainstead of a resolved 40-char SHA; the ship prompt says pass the exact SHA, never a value from memory. Impact: one ~125s non-blockingci_findtimeout, recovered viagh pr view --json headRefOid+ci_watch.other(self-identified) — anEditbatch onindex.tscarried a straynewText_unusedkey 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-reviewerruns onanthropic/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-holepoints; no error ran longer than one corrective tool call. The 74-line FAIL resolved in a single extraction. - Unused-tool detection —
pnpm fallow healthwas 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 checkafter each shared-type change, per-file red/green vitest, full suite before commits). The single gap was thefallow healthLOC/complexity check, which never ran in the TDD loop — the direct cause of the FAIL→fix cycle.
Changes made
- Appended this Final Retrospective stage entry to
packages/pi-permission-system/docs/retro/0557-serving-is-resolution.md. - Proposed adding a
pnpm fallow healthquantitative-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.