chokidar v4 holds one OS file descriptor per watched file on macOS (libuv's
kqueue backend registers an fd per vnode; fsevents is installed but v4 no
longer uses it). On a large project the `serve --mcp` daemon accumulated tens
of thousands of open REG descriptors and exhausted kern.maxfiles — crashing
unrelated processes system-wide with ENFILE. #276 only trimmed the count by
ignoring directories; the source tree still cost one fd per file.
Replace chokidar with a pure-JS native fs.watch hybrid, keeping codegraph's
zero-native-addon "any OS builds any bundle" invariant:
- macOS / Windows: a single recursive fs.watch (one FSEvents stream /
ReadDirectoryChangesW handle) -> O(1) descriptors regardless of repo size.
- Linux: one inotify watch per directory (O(dirs), dynamic add for new
dirs, capped via CODEGRAPH_MAX_DIR_WATCHES) instead of per-file watches.
Validated empirically: macOS 0 extra fds at 6k and 12k files; Linux 31 inotify
watches at 6k files (per-file would be 6k); Windows recursive catches nested
and new-directory edits. Full test suite green.
Tests drive the watcher through an inertForTests seam (no OS watcher) for
determinism under parallel vitest, with one real-fs end-to-end test exercising
the genuine native path.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>