Fixes#10488
## Problem
On Windows, `c:\Users\me` and `C:\Users\me` are the same directory, but
pi compared paths as exact strings (`===`, `startsWith`, map and set
keys). `realpathSync` keeps the input casing, so canonicalizing does not
help.
The reported case, with cwd `c:\Users\me\project` and home
`C:\Users\me`:
1. Ancestor discovery finds `c:\Users\me\.agents\skills`. The exclusion
of the user dir `C:\Users\me\.agents\skills` does not match, so global
skills load as project skills.
2. The same dir loads again as user skills. Dedup does not match either,
so every skill is reported as a name collision.
3. `hasTrustRequiringProjectResources` treats `~/.agents/skills` as a
project resource that requires trust.
The same pattern affected other places:
- Saved trust decisions in `trust.json` were not found when cwd casing
differed, so pi asked for trust again.
- With a custom session dir, `--continue`, resume by id, and the session
list hid sessions recorded with different cwd casing.
- Git worktree `AGENTS.md` shadowing failed when cwd casing differed
from git's paths.
- Local package matching (`pi remove c:\pkg` vs configured `C:\pkg`) and
package identity across global and project settings.
- Extension preload matching, skill scope classification, resource
source info, the config and session selector keys, and the session
import self-check.
Separately, home-to-`~` shortening used an unanchored `startsWith(home)`
in four places, so `/home/me2/x` was shown as `~2/x`.
## Fix
New helpers in `utils/paths.ts`, all based on one comparison key:
- `getPathComparisonKey(path)`: the resolved path, lowercased on
Windows. It does not touch the filesystem.
- `pathsEqual(a, b)`
- `isPathWithin(parent, child)`: true for equal paths or paths inside
`parent`, matching whole path segments.
- `shortenHomePath(path, home?)`: replaces a leading home dir with `~`.
The helpers pick `path.win32` or `path.posix` from `process.platform` at
call time, so tests can exercise real Windows paths (drive letters,
UNC).
`getResourcePathKey(path)` in `source-info.ts` is the dedup key for
resources. Synthetic identifiers (`builtin:<name>`, `<inline:...>`) stay
unchanged, because they are not filesystem paths and their lookup is
case-sensitive. File paths go through realpath plus the comparison key.
The helpers are used only for comparison; stored and displayed paths are
unchanged. Trust store lookups and `set` match keys case-insensitively
on Windows.
Unchanged on purpose:
- The package install-root guards (`resolveManagedPath`,
`pruneEmptyGitParents`, the `node_modules` dependency check) keep exact
string checks. Both sides come from the same root string, and exact is
the stricter option.
- Checks based on `path.relative()` (`getCwdRelativePath`, the changelog
containment check) were already correct: Node's `path.win32.relative`
ignores case.
NTFS can enable per-directory case sensitivity, but it is rarely used
outside WSL interop, so it is ignored (see the comment on
`getPathComparisonKey`).
## Tests
- `test/paths.test.ts`: Windows drive-letter, UNC and root cases, POSIX
case sensitivity, segment-boundary containment, and home shortening,
including root and UNC homes.
- `test/source-info.test.ts`: file paths fold case on Windows; builtin
and inline identifiers keep their case.
- `test/trust-manager.test.ts`: the `.agents/skills` exclusion with a
lowercase `HOME`, plus saved trust decisions matched and replaced across
casing (simulated win32).
An end-to-end test of the reported loader scenario is not feasible on
Linux: the loader resolves paths with POSIX `node:path`, and the
filesystem is case-sensitive.
`npm run check` passes. The coding-agent suite passes, except for 13
failures that also occur without this change: clipboard tool detection
in this environment, and one test that needs built `dist/` output.