14 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 597 | 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 toask. Operator confirmed: follow ADR 0008 — retire the legacy branch,ask-floor on absence. Secondask_userdecision: ship agent-scoped serving asfeat:(non-breaking), per the [#557] serving-is-resolution precedent, notfeat!:. - The resolver must accept a pre-fixed
path-valuesintent. Serving cannot go through theaccess-pathvariant (that would rebuild anAccessPathand re-derive — the exact flaw ADR 0008 removes). Resolution: widen the concretePermissionResolver.resolvetoAccessIntent | PathValuesAccessIntent(passthrough intoResolvedIntent's else-branch), keeping the gate-facingScopedPermissionResolverinterface narrow. The forwarded-serving wire becomes a second legitimate producer of pre-fixed match values — coherent with ADR-0002 (strings in, manager never importsAccessPath). - Wire→intent mapping takes primitives, not
ForwardedAccessIntent.buildResolvedIntentFromMatchValues(surface, matchValues, agentName)lives ininput-normalizer.ts(sibling ofbuildAccessIntentForSurface), soaccess-intent/stays decoupled fromauthority/. boundaryValueis not needed for rule matching —matchValues()already contains the canonical alias, soevaluateAnyValuematches a parent/tmp/**rule against the child's aliases directly;boundaryValue/requesterCwdride for provenance/disclosure only.- Atomic Step 2 is unavoidable — the
ServingPolicycheck→resolverename cascades through the server,index.ts,makeServerDeps, and theforwarded-request-server.test.tssuite; folded into onefeat:commit. The test rewrite is mechanical (mock rename + attachaccessIntent), not a full large-file rewrite. - Dead-code gate respected — the additive infra (Step 1) is exercised by its own new tests;
buildResolvedIntentFromMatchValuesis consumed byindex.tsin Step 2, never landing unused. - Composition-root round-trip tests are the behavior-parity anchor — they exercise the real serving path (real
ParentAuthorizerstampsaccessIntent, real server resolves), usedemo/no-per-agent-rules so agent-scoped lands on the sameask, 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-intentbatch 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 managercheck, the fullforwarded-request-server.test.ts, and the composition-root serving round-trip. ConfirmedasForwardedAccessIntentalready reconstructsaccessIntent(Step 2), so noforwarding-io.tschange 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.tsred was hollow at the runtime level, real at the type level.toResolvedIntent's existingelse-branch already returns any non-access-pathintent unchanged, so apath-valuesobject passed toresolver.resolveran correctly through the runtime path before the type widening — the new test passed immediately undervitest(esbuild, no typecheck). The real red wastsc:pnpm run checkfailed withTS2322on thepath-valuesliteral untilPermissionResolver.resolve/toResolvedIntentwere widened toAccessIntent | PathValuesAccessIntent. Confirmed both reds explicitly (vitest runfor the runtime file,pnpm --filter ... run checkfor 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 topolicy: { resolve }, addedaccessIntentto the requests that needed to reach the resolve branch (via the newmakeForwardedAccessIntentfixture), and split the single "floors a request without display fields" test into two: a fully-empty legacy request and a version-skew request that hassurface/valuebut noaccessIntent— 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-triptests (realParentAuthorizerstampingaccessIntent, realForwardedRequestServerresolving it,demosurface 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 theServingPolicyinterface rename. Fixed in a trivial follow-updocs: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:overfeat!:`) 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 testroot pass, 2 RPC timeouts, unrelated package, standalone re-run green both before and after this session's changes). - Release: ship now —
cross-session-intentbatch 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_usergate 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.tscase passed immediately undervitest(esbuild does not typecheck, andtoResolvedIntent's existingelse-branch already passed apath-valuesintent through), so the runtime red was hollow. The agent recognized this and confirmed the real red viapnpm run check(TS2322on thepath-valuesliteral) before implementing — no false-green slipped through. - Verification was incremental at every stage.
Green baseline before any edit;
pnpm run checkafter 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 + rootlint+fallow dead-codeafter 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) — theServingPolicycheck→resolverename left theForwardedRequestServerDeps.policyfield's doc comment describing the retired(surface, value)check. The plan enumerated two doc comments to update (class-level andresolveDecision) but missed the field-level one, and the implementation followed the plan. Impact: one pre-completion-reviewer WARN and one trivial follow-updocs: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 concurrentpnpm run testroot 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_userconfirmations (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) oranthropic/claude-sonnet-5(TDD + Ship, implementation/mechanical) — appropriate allocation, no mismatch. Themodel_changestream 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-holefriction; 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-contextgaps; 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
- 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). NoAGENTS.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. packages/pi-permission-system/docs/retro/0597-serving-resolves-forwarded-intent.md— appended this Final Retrospective stage entry.