--- issue: 597 issue_title: "pi-permission-system: serving resolves the forwarded access intent at gate parity" --- # Retro: #597 — serving resolves the forwarded access intent at gate parity ## Stage: Planning (2026-07-18T18:40:00Z) ### Session summary Planned Phase 12 Track A Step 3 — the serving node consumes the `ForwardedAccessIntent` Step 2 ([#596]) put on the wire, resolving the parent's recorded authority directly against the child-fixed `matchValues` (agent-scoped to `principal.agentName`) instead of re-deriving a path from a bare display string through the parent's `PathNormalizer`/cwd. The plan is a three-step TDD sequence (additive resolver/`buildResolvedIntentFromMatchValues` infra `feat:` → the atomic serving-rework `feat:` → a `docs:` roadmap-completion step) filed at `packages/pi-permission-system/docs/plans/0597-serving-resolves-forwarded-intent.md`. This step is the `cross-session-intent` batch tail, so it cuts the release shipping Steps 1–3. ### Observations - **Issue body vs. ADR 0008 conflict surfaced via `ask_user`.** The issue body said "keep the legacy `(surface, value)` fallback for version skew," but ADR 0008 §4 explicitly retired that branch (rejected the "Tolerant dual-path" alternative) and floors a missing-intent request to `ask`. Operator confirmed: **follow ADR 0008** — retire the legacy branch, `ask`-floor on absence. Second `ask_user` decision: ship agent-scoped serving as **`feat:`** (non-breaking), per the [#557] serving-is-resolution precedent, not `feat!:`. - **The resolver must accept a pre-fixed `path-values` intent.** Serving cannot go through the `access-path` variant (that would rebuild an `AccessPath` and re-derive — the exact flaw ADR 0008 removes). Resolution: widen the *concrete* `PermissionResolver.resolve` to `AccessIntent | PathValuesAccessIntent` (passthrough in `toResolvedIntent`'s else-branch), keeping the gate-facing `ScopedPermissionResolver` interface narrow. The forwarded-serving wire becomes a second legitimate producer of pre-fixed match values — coherent with ADR-0002 (strings in, manager never imports `AccessPath`). - **Wire→intent mapping takes primitives, not `ForwardedAccessIntent`.** `buildResolvedIntentFromMatchValues(surface, matchValues, agentName)` lives in `input-normalizer.ts` (sibling of `buildAccessIntentForSurface`), so `access-intent/` stays decoupled from `authority/`. - **`boundaryValue` is not needed for rule matching** — `matchValues()` already contains the canonical alias, so `evaluateAnyValue` matches a parent `/tmp/**` rule against the child's aliases directly; `boundaryValue`/`requesterCwd` ride for provenance/disclosure only. - **Atomic Step 2 is unavoidable** — the `ServingPolicy` `check`→`resolve` rename cascades through the server, `index.ts`, `makeServerDeps`, and the `forwarded-request-server.test.ts` suite; folded into one `feat:` commit. The test rewrite is mechanical (mock rename + attach `accessIntent`), not a full large-file rewrite. - **Dead-code gate respected** — the additive infra (Step 1) is exercised by its own new tests; `buildResolvedIntentFromMatchValues` is consumed by `index.ts` in Step 2, never landing unused. - **Composition-root round-trip tests are the behavior-parity anchor** — they exercise the real serving path (real `ParentAuthorizer` stamps `accessIntent`, real server resolves), use `demo`/no-per-agent-rules so agent-scoped lands on the same `ask`, and stay green unchanged. - **[#565] does not close here** — Step 3 structurally dissolves items 2–3, but [#565] stays open until Phase 12 end per roadmap decision. - **Release**: ship now — `cross-session-intent` batch tail. - Next stage is `/tdd-plan` (the plan has red→green→commit cycles). ### Diagnostic details - **Feedback-loop gap analysis** — grounded every design claim in source before writing: read `forwarded-request-server.ts`, `permission-forwarding.ts`, `permission-resolver.ts`, `access-intent.ts`, `input-normalizer.ts`, `forwarding-io.ts`, `index.ts` (serving closure), the manager `check`, the full `forwarded-request-server.test.ts`, and the composition-root serving round-trip. Confirmed `asForwardedAccessIntent` already reconstructs `accessIntent` (Step 2), so no `forwarding-io.ts` change is needed. ## Stage: Implementation — TDD (2026-07-18T19:15:00Z) ### Session summary Executed all three plan steps as planned, no Tidy-First preparatory refactors landed (the `tidy-first-assessor` found the target files already shaped for the change). Four commits: additive `feat:` (resolver `path-values` acceptance + `buildResolvedIntentFromMatchValues`), the atomic `feat:` serving rework (`ServingPolicy` intent-shaped, legacy branch retired), a `docs:` roadmap-completion commit, and a small follow-up `docs:` commit fixing a stale doc comment the pre-completion reviewer flagged. Test count 2491 → 2499 (+8). Pre-completion reviewer returned WARN (one cosmetic finding), fixed before finishing. ### Observations - **The Step-1 `permission-resolver.test.ts` red was hollow at the runtime level, real at the type level.** `toResolvedIntent`'s existing `else`-branch already returns any non-`access-path` intent unchanged, so a `path-values` object passed to `resolver.resolve` ran correctly through the *runtime* path before the type widening — the new test passed immediately under `vitest` (esbuild, no typecheck). The real red was `tsc`: `pnpm run check` failed with `TS2322` on the `path-values` literal until `PermissionResolver.resolve`/`toResolvedIntent` were widened to `AccessIntent | PathValuesAccessIntent`. Confirmed both reds explicitly (`vitest run` for the runtime file, `pnpm --filter ... run check` for the type-level file) before calling it Red, per the testing skill's "hollow red" warning. - **Step 2 test rewrite was exactly as mechanical as planned.** Renamed every `policy: { check }` mock to `policy: { resolve }`, added `accessIntent` to the requests that needed to reach the resolve branch (via the new `makeForwardedAccessIntent` fixture), and split the single "floors a request without display fields" test into two: a fully-empty legacy request and a version-skew request that has `surface`/`value` but no `accessIntent` — the literal ADR 0008 §4 scenario, which the original single test didn't distinguish. No large-file rewrite needed a lift-and-shift; the whole file fit one atomic commit. - **Composition-root round-trip anchors held unchanged, as predicted.** Both `forwarded grant-scope selection round-trip` tests (real `ParentAuthorizer` stamping `accessIntent`, real `ForwardedRequestServer` resolving it, `demo` surface with no per-agent rules) passed without any edit — confirming agent-scoped resolution is a true superset of the agent-neutral serving it replaced. - **Pre-completion reviewer: WARN, one finding** — a stale doc comment on `ForwardedRequestServerDeps.policy` (`"Recorded-authority resolution for \`(surface, value)\` requests."`) that survived the `ServingPolicy` interface rename. Fixed in a trivial follow-up `docs:` commit (not amended into the feat commit, since the feat commit was already several commits back and AGENTS.md discourages non-interactive rebase for reordering). Reviewer explicitly verified the implementation matched the plan's two operator-confirmed deviations from the issue body (retire-outright over dual-path; `feat:` over `feat!:`) rather than flagging them as drift. - **Reviewer note (non-issue):** the same pre-existing pi-autoformat acceptance-test flake noted in the #596 retro recurred (`pnpm run test` root pass, 2 RPC timeouts, unrelated package, standalone re-run green both before and after this session's changes). - **Release**: ship now — `cross-session-intent` batch tail (Steps 1–3). Next step is `/ship-issue`. ## Stage: Final Retrospective (2026-07-18T22:57:31Z) ### Session summary Shipped issue #597 end to end in one continuous session — planned Phase 12 Track A Step 3, executed three TDD cycles (no Tidy-First prep needed), handled the pre-completion reviewer's one WARN, then pushed, verified CI, closed the issue, and merged the release-please PR to cut `pi-permission-system` 20.8.0 (the `cross-session-intent` batch, Steps 1–3). The change makes the serving node resolve a forwarded permission request against the child-fixed `ForwardedAccessIntent.matchValues` (agent-scoped), retiring the legacy `(surface, value)` re-derivation and structurally dissolving [#565] items 2–3. An exceptionally low-friction session — the only substantive user input across all four stages was the two `ask_user` design confirmations at planning time. ### Observations #### What went well - **The planning `ask_user` gate caught a real issue-body-vs-accepted-ADR contradiction (novel win).** The issue body said "keep the legacy `(surface, value)` fallback for version skew," but the operator's own accepted ADR 0008 §4 had already retired that branch (rejecting the "Tolerant dual-path" alternative with rationale). Surfacing it rather than transcribing the issue produced a materially different — and correct — plan: the operator confirmed "follow the ADR," which drove the whole implementation (sole-resolution-path, `ask`-floor on absence). This is the "issue body is a hypothesis, not a spec" principle plus "accepted ADR is authority" working exactly as intended. - **The testing skill's "hollow red" guidance paid off at Step 1.** The new `permission-resolver.test.ts` case passed immediately under `vitest` (esbuild does not typecheck, and `toResolvedIntent`'s existing `else`-branch already passed a `path-values` intent through), so the runtime red was hollow. The agent recognized this and confirmed the *real* red via `pnpm run check` (`TS2322` on the `path-values` literal) before implementing — no false-green slipped through. - **Verification was incremental at every stage.** Green baseline before any edit; `pnpm run check` after each shared-signature change (the resolver widening in Step 1, the interface rename in Step 2); the target test file after every red→green; full suite + root `lint` + `fallow dead-code` after the last step and again pre-push. The feedback-loop-gap lens found no deferred-verification gap. - **The plan's Module-Level Changes matched the actual diff exactly** — the atomic Step 2 scope (server + `index.ts` + fixtures + test suite in one commit) was correctly predicted, and the composition-root round-trip anchors stayed green unchanged, confirming agent-scoped serving is a true superset of the agent-neutral serving it replaced. #### What caused friction (agent side) - `other` (doc-comment staleness during an interface rename) — the `ServingPolicy` `check`→`resolve` rename left the `ForwardedRequestServerDeps.policy` field's doc comment describing the retired `(surface, value)` check. The plan enumerated two doc comments to update (class-level and `resolveDecision`) but missed the field-level one, and the implementation followed the plan. Impact: one pre-completion-reviewer WARN and one trivial follow-up `docs:` commit (`64b1b8e1`); no behavior rework. Self-caught by the reviewer doing exactly its job — the right backstop for cosmetic doc staleness. - `other` (recurring environmental flake) — the pi-autoformat real-CLI acceptance tests (`test/acceptance.test.ts`) timed out under the concurrent `pnpm run test` root run (2 RPC timeouts), the same flake the #596 retro recorded. Impact: ~3 confirmation tool-calls per occurrence (standalone re-run to prove it unrelated); no rework, resolved correctly using the #596 retro context. Now observed across two consecutive sessions — filed as [#618] for a durable fix rather than per-session re-confirmation (see Changes made). #### What caused friction (user side) - **None — oversight was minimal and well-placed.** The operator's only substantive interventions were the two planning-stage `ask_user` confirmations (the legacy-fallback and commit-type decisions), exactly the strategic-judgment calls the workflow reserves for a human; everything else ran unattended from a plan the operator had reviewed. ### Diagnostic details - **Model-performance correlation** — every rendered parent-session turn ran on `anthropic/claude-opus-4-8` (Planning + Retrospective, judgment-heavy) or `anthropic/claude-sonnet-5` (TDD + Ship, implementation/mechanical) — appropriate allocation, no mismatch. The `model_change` stream also showed several switches (`deepseek-v4-flash`, `claude-fable-5`, `claude-haiku-4-5`) that never ran a turn (no `[provider/model]` label appears on any rendered turn for them), so no reasoning-weak model executed judgment-heavy work. The two subagent dispatches (`tidy-first-assessor`, `pre-completion-reviewer`) ran on their configured models for judgment tasks — appropriate. - **Escalation-delay tracking** — no `rabbit-hole` friction; the hollow-red at Step 1 resolved in a single follow-up tool call (`tsc`), well under the 5-call threshold. - **Unused-tool detection** — no `missing-context` gaps; planning read every relevant source file directly (the design was fully grounded before the plan was written), so implementation needed no exploratory search. - **Feedback-loop gap analysis** — verification was incremental at every stage (see "What went well"); no lens found a gap. ### Changes made 1. Filed [#618] (`pkg:pi-autoformat`) to harden the real-CLI acceptance tests against the concurrent-workspace-run RPC timeout flake — the one durable cross-session pattern (#596 + #597). No `AGENTS.md`/prompt rule was added: the doc-comment miss was a self-caught one-off (reviewer backstop), and documenting the flake as a workaround was rejected in favor of the root-cause fix. 2. `packages/pi-permission-system/docs/retro/0597-serving-resolves-forwarded-intent.md` — appended this Final Retrospective stage entry. [#618]: https://github.com/gotgenes/pi-packages/issues/618