11 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 504 | pi-permission-system: retire input-normalizer path normalization (Phase 7 Step 3) |
Retro: #504 — Retire input-normalizer path normalization (Phase 7 Step 3)
Stage: Planning (2026-06-29T18:30:00Z)
Session summary
Planned Phase 7 Step 3 (batch "symlink-resistant-path-matching" tail): remove the dead lexical-only path normalization from input-normalizer.ts left behind by Steps 1 (#502) and 2 (#503).
Verified against live code that normalizePathSurfaceValues, the special-surface (path / external_directory) branch, and the PATH_BEARING_TOOLS branch of normalizeInput have no remaining production caller for a real path value — every path surface now routes through access-path → resolver → path-values.
Produced a four-step plan (two test: steps, one refactor:, one docs:) at docs/plans/0504-retire-input-normalizer-path-normalization.md.
Observations
- The change is non-breaking despite riding a breaking batch.
The tail commit is a pure
refactor:(dead-code removal); the missing-path / empty-input case for path-bearing tools falls through to the generic["*"]branch with an identical result. The release is cut by Steps 1 and 2's already-landed breakingfeat!:commits, not this refactor — so theRelease Recommendationis "ship now — batch tail" with a rationale that the refactor itself does not trigger a release (Refs #479). - The bulk of the work is a faithful test migration, not the production removal.
test/permission-manager-unified.test.tshas ~30checkTool(manager, <path-surface>, { path })calls that rely on the doomednormalizeInputpath branches; they migrate to a newcheckPathhelper routing through thepath-valuesintent viagetPathPolicyValues(path, opts, "linux")— identical values, green against current production (tidy-first preparatory Step 1). The ~39 empty-inputcheckTool(manager, <surface>, {})calls produce["*"]and are unaffected. - Confirmed the migration surface is confined to one test file.
All other
kind: "tool"test usages (permission-resolver.test.ts,permission-event-rpc.test.ts,service.test.ts,skill-prompt-sanitizer.test.ts) usebash/skillsurfaces; onlypermission-manager-unified.test.tsdrivestoolintents for path surfaces. currentCwdremoval is forced by the dead-code gate. Grep confirmedpermission-manager.ts'scurrentCwdfield is read only by thenormalizeInputcall being changed;configureForCwdrebuilds the loader from itscwdparameter directly. Dropping the field (and theplatform/cwdparams fromnormalizeInput) avoids apnpm fallow dead-codefailure — the #502 lesson that the baseline check/lint/test triad does not see dead members or stale suppressions.- Signature change cascades to test arity.
Dropping
platform/cwdfromnormalizeInputbreaks everynormalizeInput(..., "linux")call attsctime (esbuild skips it), so the arity fix rides in therefactor:green commit, withpnpm run checkimmediately after. - Doc rewrite needed, not just a
✅.architecture.md's### Path-bearing tool normalizationsection still attributes per-tool path matching tonormalizeInput; it must be reworded to theaccess-pathmechanism (#502) — reworded prose carries no removed symbol, so it would not surface in asrc/-symbol grep. Also flagged SKILL.md line ~130's deferred-follow-up note that namesnormalizeInputas the threading mechanism. - Skipped the
ask_usergate: operator-authored issue, unambiguous and roadmap-blessed proposal, consistent with the #502 / #503 precedent in this batch.
Stage: Implementation — TDD (2026-06-29T21:15:00Z)
Session summary
Implemented all four planned TDD steps: Step 1 migrated ~50 real-path checkTool calls in permission-manager-unified.test.ts to a new checkPath helper (green against production); Step 2 replaced three dead describe blocks in input-normalizer.test.ts with 4 red assertions asserting the new ["*"]-only contract; Step 3 removed the dead branches, dropped platform/cwd from normalizeInput and currentCwd from PermissionManager (green, tsc + full suite + lint + fallow); Step 4 updated architecture.md and SKILL.md and marked Phase 7 Step 3 ✅.
Test count 2222 → 2198 (−24 deleted input-normalizer tests, +4 new contract assertions = −20 net from the normalizer, plus 4 extra missed-path corrections).
Pre-completion reviewer returned WARN with two findings, both resolved before the retro commit.
Observations
- The plan's ~30 estimate was low — actual migration surface was ~50 calls.
Planning identified multi-line
checkToolcalls where the surface name is on the next line, and single-line ones where the surface is inline, but missed four calls where the surface (external_directory) was embedded in larger multi-line patterns with session rules or agent names (lines 171, 2676, 2686, 2872 / 3005 in the original file). These surfaced as 4 unexpected failures when the full suite ran after Step 3; fixed as part of the same step before committing. - A structural repair was needed mid-Step 2.
One
Editcall used"universal '*': 'deny'"as an anchor, which split the test's string literal at the embedded'deny'quote, corrupting thedescribeblock structure. Detected immediately bypnpm exec biome check; repaired via a targeted Python line-range replacement since the exact bytes (with trailing})closure) didn't match any cleanEditanchor. Lesson: when a test description contains smart quotes or embedded single-quotes, use a wider anchor that includes a few surrounding lines rather than the string literal alone. - Step 1 test migration was the largest work unit.
The
checkPathhelper matched the plan design exactly; thegetPathPolicyValues(path, cwd ? { cwd } : {}, "linux")call was faithful to the oldnormalizeInputpath branch. The two cwd-aware tests (lines 3302–3352) correctly passed bothmanager.configureForCwd(cwd)(for loader) and{ cwd }tocheckPath(for alias derivation). - Pre-completion reviewer WARN findings (both fixed before commit):
docs/architecture/architecture.mdline ~786: per-tool gate bullet in the Findings "residual ad-hoc path handling" section lacked a "closed by" annotation — added— closed by Steps 1–3 ([#502], [#504]).test/input-normalizer.test.ts:import { join } from "node:path"was unused after removing the home-expansion test blocks; Biome had auto-fixed it during Step 3 but the fix wasn't staged before commit — removed explicitly and committed.
Stage: Final Retrospective (2026-06-29T18:38:34Z)
Session summary
Shipped Phase 7 Step 3 across plan → TDD → ship, completing the three-step "symlink-resistant-path-matching" batch as a single pi-permission-system v18.0.0 major release.
The ship stage merged release-please PR #515 (UNSTABLE / no-checks GITHUB_TOKEN case → gh pr merge --rebase) and closed all three batch issues (#504, #502, #503), recognizing #513 was already closed.
A clean run end to end — no rework, no rabbit-holes; the only friction (a low call-site estimate, an Edit string-truncation) was self-caught within the same commit.
Observations
What went well
- The ship prompt is deterministic enough that a weak model executed it flawlessly.
The entire ship stage ran on
opencode-go/deepseek-v4-flash(a low-cost model), yet it correctly read the plan's**Release:** ship now — batch tailmarker without asking, navigated the nuancedrelease_pr_mergeUNSTABLE refusal by checkingstatusCheckRollup(empty → no checks →gh pr merge 515 --rebase), and closed three issues with batch-aware summaries. This validates the ship prompt's step-by-step protocol: the judgment is encoded in the prompt, not delegated to the model. - Batch-tail release coordination worked exactly as designed across three issues.
The plan's
**Release:**marker drove the no-ask release decision; the already-landed breakingfeat!:commits from Steps 1–2 cut the major bump when thisrefactor:/docs:/fix:tail landed; and reading the #502 retro notes prompted closing #502, #503, and verifying #513 — none of which leave arefactor:changelog reminder. - The plan's risk mitigation caught its own scope miss.
The plan under-counted the migration surface (~30 vs. ~50 real calls), but its stated mitigation — "any unmigrated real-path
checkToolsurfaces as a red in Step 3's full-suite run" — fired exactly as written: 4 unexpected failures appeared at the Step 3 full-suite run and were fixed in the same commit, no separate rework.
What caused friction (agent side)
missing-context(planning) — the plan'scheckToolcall-site count (~30) was derived from a single-line grep (grep 'checkTool(manager, "<surface>"'), which cannot match the 4 multi-line invocations wherecheckTool(,manager, and the surface literal sit on separate continuation lines. Impact: 4 unexpected test failures at the Step 3 full-suite run; fixed within the same commit (no separate rework), because the plan's full-suite mitigation caught them.other(Edit-tool string truncation) — one Step 2Editproduced anewTextthat terminated a test description string early ("universal '*': 'deny'"from"universal '*': 'deny' with no path key …"), corrupting thedescribeblock. Impact: one structural-repair detour via a Python line-range replacement; caught immediately bybiome check, no rework beyond the repair. Self-identified.
What caused friction (user side)
- None.
The single decision point (batch-tail release) was pre-resolved from the plan's
**Release:**marker, so the ship ran without anask_userinterrupt — the intended handshake for a non-deferred batch tail.
Diagnostic details
- Model-performance correlation — the ship stage ran on
opencode-go/deepseek-v4-flashand thepre-completion-reviewer(TDD stage) onanthropic/claude-sonnet-4-6per its frontmatter; both appropriate. No reasoning-weak-model-on-judgment-work mismatch: the ship prompt encodes the judgment, so the weak model only had to follow steps. - Escalation-delay tracking — no
rabbit-hole; the longest same-target sequence was 2 grep retries (entries 4–6) probingrelease-please-config.jsonexclude-paths, resolved immediately. - Unused-tool detection — no gap warranted an Explore/
colgrepdispatch; the planningmissing-contextmiss was a grep-pattern limitation, not a missing tool. - Feedback-loop gap analysis — verification was incremental in TDD (per-file red→green,
pnpm run checkright after the signature change, full suite + lint +fallowafter Step 3); the ship stage's CI watch + release watch ran in protocol order. No end-loaded verification.
Changes made
.pi/skills/testing/SKILL.md— added a rule under### Interface and type changes: when estimating the call-site count for a test migration, grep the bare callee (checkTool(), notcallee(arg, "literal", since a single-line pattern misses multi-line invocations and undercounts scope (the planningmissing-contextmiss above).