Files
my-pi/pi-permission-system/docs/retro/0557-serving-is-resolution.md
T

11 KiB
Raw Blame History

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.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 (prompterauthorizerSelectionresolverservingPolicyrequestServer), 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 34 (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 detectionpnpm 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.