8.8 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 511 | pi-permission-system: retire the residual getPlatform() threading (infra-read + skill-prompt sanitization) |
Retro: #511 — Retire the residual getPlatform() threading (infra-read + skill-prompt sanitization)
Stage: Planning (2026-06-28T23:10:40Z)
Session summary
Produced a numbered plan to route the two residual platform-threading sites (external-directory infra-read and skill-prompt sanitization) through the session's PathNormalizer, adding two methods (comparableValue, isInfrastructureRead) and dropping the platform parameters from describeExternalDirectoryGate, describeSkillReadGate, and the skill-sanitizer functions.
Filed follow-up #513 for the final getPlatform() removal, which neither #511 nor #502 can complete alone (the pipeline reads getPlatform() once and threads it to three gates; the tool gate keeps it until #502 lands).
Observations
- The issue raises a genuine fork for skill sanitization (carry
AccessPaths vs. resolve through the normalizer). Resolved decisively against carryingAccessPaths:AccessPath.forPatheagerly computes the canonical alias viarealpathSync(canonicalize-path.ts), so it would add per-entry, per-turn filesystem access and shift lexical matching toward canonical — both behavior changes against a behavior-preserving refactor. Chose a lexicalcomparableValuemethod instead. Did not invokeask_userbecause the behavior-preservation invariant removes the real choice (one option violates it). - Confirmed
tcc.cwd === ctx.cwd === normalizer.cwd(set inpermission-gate-handler.tsand rebuilt onsession.activate), which is what makes moving thecwdargument onto the baked normalizer behavior-preserving. isInfrastructureReadtakes the already-builtAccessPath(not a raw path) so the gate does not re-runforPath/realpathSync— Tell-Don't-Ask plus no double FS.- Steps 1–4 of Phase 7 (#502–#505) are all still OPEN, so this plan deliberately leaves
getPlatform()in place rather than removing it; the dependency, not ambiguity, forces that. getPlatform()removal depends only on #511 + #502 (the only twogetPlatform()readers arebefore-agent-start.tsand the pipeline; the pipeline's read survives for the tool gate until #502). It does not depend on #503/#504, which touchinput-normalizer.ts(that file getsplatformelsewhere, not viagetPlatform()).- Doc surface to update:
architecture.md(normalizer method list + the residual-threading subsection) and the packageSKILL.md(normalizer method list in Debugging). Plans/retros underdocs/are historical and left untouched.
Stage: Implementation — TDD (2026-06-28T19:32:00Z)
Session summary
Completed all four planned TDD cycles: added PathNormalizer.comparableValue + isInfrastructureRead, routed the external-directory infra-read through the normalizer, routed skill-prompt sanitization through the normalizer, and updated the architecture doc + package SKILL.md.
Test count went 2189 → 2195 (+6 new PathNormalizer tests); full suite, check, lint, and fallow dead-code all green.
Pre-completion reviewer returned PASS.
Observations
- Pre-completion reviewer: PASS. No WARN findings.
- Deviation: also updated
test/handlers/external-directory-symlink-acceptance.test.ts(not in the plan's listed test files) — it threaded the sameplatformarg intodescribeExternalDirectoryGate;pnpm run checkcaught it after the Step 2 production edit. Noted in the Step 2 commit body. - The plan's lexical-no-FS decision held up:
comparableValuedelegates tonormalizePathForComparison(norealpathSync), andforPathis intentionally absent from all skill-entry code paths. - Self-inflicted friction: a blind
sed 's/"linux"/normalizer/g'over the two skill test files also rewrote the"linux"inside thenew PathNormalizer("linux", cwd)const declarations themselves (→new PathNormalizer(normalizer, cwd)), which surfaced as aReferenceError: Cannot access 'normalizer' before initialization. Fixed with a follow-up targetedsed. Lesson: exclude the constructor lines (or add the const after the bulk replacement) when mass-replacing a platform literal. getPlatform()left in place as planned (still feedsdescribeToolGateuntil #502); theToolCallGatePipeline.evaluateconst platformread remains. Final removal tracked in #513.- biome's pre-commit hook reflowed the Step 3 sanitizer test (collapsed now-short multi-line calls); re-staged and committed cleanly.
Stage: Final Retrospective (2026-06-28T23:53:50Z)
Session summary
Shipped #511 end-to-end in one continuous session (plan → TDD → ship): a behavior-preserving refactor routing the two residual platform-threading sites through PathNormalizer, landing 4 commits, +6 tests (2189 → 2195), pre-completion PASS, CI green, issue closed.
No release was cut — every commit is refactor: (hidden changelog type) or docs: on an exclude-paths directory, so the work auto-batches into the next releasing commit, exactly as the plan's **Release:** ship independently rationale predicted.
The session was clean: the two friction points were minor, self-inflicted, and each caught immediately by a deterministic check (tsc / a test run).
Observations
What went well
- Hard-invariant resolution of an issue-flagged "decision."
The issue explicitly forked skill sanitization (carry
AccessPaths vs. route through the normalizer). The behavior-preservation invariant (AccessPath.forPatheagerly callsrealpathSyncviacanonicalize-path.ts) made one branch a behavior change, collapsing the fork to a single correct answer (a lexicalcomparableValuemethod) without spending anask_userround. Recognizing when a constraint — not preference — settles a flagged decision kept planning tight. - Proactive cross-issue follow-up (#513).
The final
getPlatform()removal cannot be completed by #511 or #502 alone (the pipeline reads it once and threads it to three gates; the tool gate keeps it until #502). Filing #513 during planning gave that order-independent cleanup an explicit home rather than leaving an untracked dangling accessor. - Incremental feedback loop did its job.
pnpm run checkran after each signature-changing step and caught the one un-planned test file (external-directory-symlink-acceptance.test.ts) the moment Step 2's production edit landed — no end-of-session surprise. - Pre-completion reviewer PASS on first dispatch (ran on
anthropic/claude-sonnet-4-6, model well-matched to judgment-heavy review).
What caused friction (agent side)
missing-context(self-identified) — the plan's Module-Level Changes listed onlyexternal-directory.test.tsfor the external-directory gate, but the planning-stagegrep -rln describeExternalDirectoryGate test/had already surfacedexternal-directory-symlink-acceptance.test.tstoo. The grep result was in hand; it just wasn't carried into the plan's test-file list. Impact: one extrasedfix during TDD Step 2, caught instantly bytsc. No rework beyond that; noted in the Step 2 commit body.other(self-identified) — a blindsed 's/"linux"/normalizer/g'over the two skill test files also rewrote the literal inside thenew PathNormalizer("linux", cwd)const declarations I had just added (→new PathNormalizer(normalizer, cwd)), producingReferenceError: Cannot access 'normalizer' before initialization. Impact: ~2 extra tool calls (one failed test run + one targetedsed). Self-caught on the next test run. Lesson: when mass-replacing a literal, guard the new symbol's own declaration (replace before introducing the const, or exclude the constructor line).
What caused friction (user side)
- None. The session ran autonomously across all three stages with no user correction or redirect needed.
Diagnostic details
- Model-performance correlation — one subagent dispatched (pre-completion-reviewer) on
anthropic/claude-sonnet-4-6; appropriate for judgment-heavy review (acceptance criteria, design, invariants). No mismatch. - Feedback-loop gap analysis — no gap:
pnpm run checkran after each shared-signature step, the affected test file ran per Red/Green cycle, and the full suite +lint+fallowran once before the pre-completion dispatch. Verification was incremental, not deferred. - Escalation-delay and unused-tool lenses found nothing notable (no rabbit-hole exceeded 1 follow-up tool call; no missing-context warranted a subagent).
Changes made
- Added this Final Retrospective stage entry to
packages/pi-permission-system/docs/retro/0511-retire-residual-getplatform-threading.md. NoAGENTS.mdor prompt changes — the session's two friction points were minor, self-caught by deterministic checks, and already covered by existing guidance (confirmed with the operator).