SIGN IN SIGN UP

fix(locks): canonicalize paths so one file cannot yield two lock keys

Found by running examples/two-agent-collision.ts, which was the first thing to
exercise the lock path from outside the test harness.

Symlinks were not resolved. On macOS /var is a symlink to /private/var, so one
physical file reached by two valid names produced two different lock keys. The
loud version of this is a refused lock. The dangerous version is the opposite:
two developers reaching one file through different symlinked paths both get
GRANTED and the lock system silently does not lock. normalizeLockPath (the key
generator) and validateFileOnly (the containment check) now both canonicalize,
resolving symlinks down to the deepest existing ancestor so files that do not
exist yet can still be locked.

projectRoot was accepted as a NerveCenter option and never reached lock
validation, which always used process.cwd(). Any server whose working directory
is not the repo root refused every lock. It is carried on CoreServices as a live
getter, not a snapshot, because detectProjectName can reassign the root to a
discovered ancestor after construction.

Three regression tests, all verified to fail against the pre-fix code. One of
them initially passed against broken code for the wrong reason: the symlinked
path returned REJECTED (bogus containment failure) rather than
REQUIRES_ORCHESTRATION (real conflict), which satisfied a loose not.toBe(GRANTED)
while proving nothing. It now pins the status and requires the holder in the message.

Follow-up: switchProject calls process.chdir, which partly masked this. Nothing in
the lock path needs it now and it is worth removing.
V
Vir Sanghavi committed
35932f25e55d1462d39d989f027eb36ef74b44f1
Parent: 1c4a965