Files
my-pi/pi-permission-system/docs/retro/0382-windows-external-directory-case-insensitive.md
T

9.9 KiB
Raw Blame History

issue, issue_title
issue issue_title
382 pi-permission-system: external_directory base permission doesn't auto-detect or allow overrides for pi docs directory when installed via npm on Windows

Retro: #382 — Windows case-insensitive external_directory matching and Pi-install auto-detect

Stage: Planning (2026-06-10T00:00:00Z)

Session summary

Traced the reported bug to a Windows-only path-comparison asymmetry: the path under test is canonicalized and lowercased on win32 (normalizePathForComparison / canonicalNormalizePathForComparison), but infra-dir containment (isPathWithinDirectory, case-sensitive startsWith) and config-pattern matching (compileWildcardPattern, case-sensitive RegExp) keep native case — so both the infrastructure auto-allow and explicit external_directory overrides silently fail. Produced docs/plans/0382-windows-external-directory-case-insensitive.md with a 6-step TDD order covering path.relative containment, case/separator-folded path-surface matching, and Pi-install auto-detect via getPackageDir().

Observations

  • The user steered the design with two questions ("is there a builtin node path library?" / "how does pi handle this itself?"). Confirmed path.win32.relative folds case natively, and Pi's own idiom (getCwdRelativePath, getPiDocsClassification in packages/coding-agent/src/utils/paths.ts and core/tools/read.ts) is relative() + ../absolute check with no manual lowercasing — adopted as the containment approach.
  • path.matchesGlob was rejected: its * does not cross separators and it is case-sensitive even on win32, so it would change the established *.* semantics without fixing the case bug.
  • Two ask_user calls settled scope: (1) comparison fix plus pi-API auto-detect, (2) adopt path.relative broadly; a follow-up picked getPackageDir() (whole Pi install dir) over docs-only paths.
  • Key dependency constraint: getPackageDir() / getDocsPath() are only re-exported from @earendil-works/pi-coding-agent's entry point as of v0.79.0 (commit eb43bd44); the installed devDependency 0.75.4 exports only getAgentDir + VERSION. The plan therefore bumps the peer floor >=0.75.0>=0.79.0 (the reporter runs 0.79.1).
  • Testability decision: stubbing process.platform does not switch Node's top-level path functions to win32, so production code selects path.win32/path.posix from an injected, defaulted platform parameter and tests pass "win32" + C:\… paths. This also satisfies the AGENTS.md "no process.platform inside library functions" guidance.
  • evaluate in rule.ts is the single surface-aware matching site; folding is scoped to a new exported PATH_SURFACES set (PATH_BEARING_TOOLS { external_directory, path }) so bash/skill/mcp stay case-sensitive.
  • Classified as a non-breaking fix: — POSIX behavior is unchanged and the peer bump does not alter runtime behavior/config on upgrade.
  • Deferred (non-goals): removing the now-redundant win32 lowercasing in normalizePathForComparison, and dissolving subagent-context.ts's duplicate containment helper.

Stage: Implementation — TDD (2026-06-10T19:17:00Z)

Session summary

Implemented the Windows case-insensitive path-matching fix across 7 commits (6 TDD cycles + 1 docs): path.relative-based containment in isPathWithinDirectory, WildcardMatchOptions (case-insensitive + Windows-separator folding) on the matcher, case-insensitive infra-read auto-allow, path-surface case folding in evaluate via a new PATH_SURFACES set, an optional piPackageDir on computeExtensionPaths, and getPackageDir() wiring at the composition root (with the @earendil-works/pi-coding-agent / pi-tui floor bump >=0.75.0>=0.79.0, devDeps 0.79.1). Test count went from 1902 to 1921 (+19); full suite, check, lint, and fallow dead-code all green.

Observations

  • Pre-completion reviewer: PASS.
  • Reviewer warnings: one non-blocking WARN — evaluateFirst / evaluateMostRestrictive delegate to evaluate without exposing platform, so they are not unit-testable for Windows case-folding on a POSIX CI (runtime behavior is correct because process.platform is win32 in production; the Windows path is covered compositionally by the evaluate-level tests). Left as-is; a future change could thread platform through them if dedicated Windows coverage is wanted.
  • Deviation 1: did not thread platform into isPathOutsideWorkingDirectory (the plan suggested it). Its internal isPathWithinDirectory call already captures the runtime platform via the default param, and the win32 path there is not unit-testable on a POSIX CI because canonicalizePath splits on /. Avoided an untested parameter.
  • Deviation 2: reordered the plan's steps — the wildcard-matcher options had to land before isPiInfrastructureRead consumed them (the plan listed infra-read first and the matcher options later). A real dependency-ordering correction.
  • Deviation 3: skipped the plan's brittle end-to-end external-directory gate integration test (Red C). Flipping process.platform does not switch Node's path.win32 dispatch on a POSIX runner, and the gate's canonicalNormalizePathForComparison only lowercases on real win32, so a darwin-runner integration test would be unreliable. Coverage is provided by the composed unit tests at the evaluate / wildcard / path-utils levels.
  • The pnpm install after the dep bump printed "Already up to date" but did update pnpm-lock.yaml (+205 lines adding 0.79.1); verified the installed index.d.ts re-exports getPackageDir before wiring index.ts.

Stage: Final Retrospective (2026-06-10T20:30:00Z)

Session summary

Shipped the Windows case-insensitive external_directory fix end-to-end across planning, TDD, and release: 7 implementation commits, +19 tests, pi-permission-system v10.10.0 released and issue #382 closed. The session was clean throughout — no rabbit-holes, no instruction violations, all gates green on the first push, and a PASS pre-completion review. The one dominant friction was a planning-stage missing-context gap that the user redirected with two well-aimed questions.

Observations

What went well

  • The injected-platform-parameter testability pattern (select path.win32/path.posix from a defaulted platform arg, pass "win32" + C:\… in tests) cleanly solved the "can't stub process.platform to switch Node's path dispatch" problem. Reusable for any cross-platform path logic.
  • Empirical verification before designing: a throwaway node -e script confirmed path.win32.relative folds case, and reading the local pi checkout (~/development/pi/pi) surfaced getCwdRelativePath / getPiDocsClassification / getPackageDir before committing to an approach.
  • Tight feedback loop: vitest run <file> after every Red and Green, pnpm run check after each type-affecting step, full suite before commits touching shared helpers (isPathWithinDirectory, evaluate). No end-of-session verification surprises.
  • Execution-time adaptation: caught a plan dependency-ordering inversion (the wildcard-matcher options had to land before isPiInfrastructureRead consumed them) and reordered without rework.

What caused friction (agent side)

  • missing-context — during planning I converged on a hand-rolled case-folding fix (lowercase both sides / case-insensitive regex) without first checking whether Node's path module had a containment primitive or how the upstream host @earendil-works/pi-coding-agent solves the same problem. The user's two questions ("is there a builtin node path library?" / "how does pi handle this itself?") supplied exactly the missing checks, which redirected the design to path.win32.relative (native case-folding) and getPackageDir() (robust auto-detect). Self-identified: no (user-caught, via redirecting questions). Impact: no rework — caught in planning before the plan was written — but without the redirect the plan would have shipped more complex hand-rolled containment instead of the cleaner builtin-based design.
  • other (plan dependency-ordering miss) — the plan listed the infra-read folding step before the wildcard-matcher options step it depended on. Self-identified at execution; reordered with no rework.

What caused friction (user side)

  • The high-leverage context (prefer Node builtins; check how pi-coding-agent itself solves filesystem/platform problems) arrived as a mid-planning redirect rather than being available up front. Framed as opportunity: encoding "check pi-coding-agent's implementation first for path/platform bugs" in the package skill lets the agent do this proactively without the redirect. The intervention style — two redirecting questions instead of a correction — was ideal and worth preserving.

Diagnostic details

  • Model-performance correlation — one subagent dispatched (pre-completion-reviewer, judgment-heavy review). It returned a structured PASS with a genuine non-blocking WARN (the evaluateFirst/evaluateMostRestrictive testability gap) — appropriate quality for the task; no model/task mismatch.
  • Escalation-delay tracking — no rabbit-hole friction points; longest run on a single error was the expected Red→Green cycle (12 tool calls).
  • Unused-tool detection — the missing-context gap was not a tool-usage failure: once pointed at ~/development/pi/pi, exploration via grep/Bash/node -e was efficient. The gap was "did not think to check the upstream host implementation," not "checked it inefficiently."
  • Feedback-loop gap analysis — no gap; verification ran incrementally after each change, not deferred to the end.

Changes made

  1. .pi/skills/package-pi-permission-system/SKILL.md — added a 4th item to the Debugging section: for path/filesystem/platform bugs, check how @earendil-works/pi-coding-agent solves it first and prefer Node path builtins over hand-rolled comparison.