SIGN IN SIGN UP

fix(watch): discover directories by polling, not by directory events

Review of the first cut found that adoption of new directories could never
work on macOS: `FSEventsEmitter._is_recursive_event` drops every directory
event whose path is not the watch path itself, and every file event whose
dirname is not the watch path. A probe confirms it — under a non-recursive
watch, `mkdir child` and `child/file.txt` produce no events at all, while a
file at the watch root arrives normally. The earlier end-to-end run that
appeared to prove adoption was invalid: a hung previous run had already
created the directory, so the watcher planned it at startup rather than
adopting it.

Correctness no longer depends on events:

- the supervisor diffs one `scandir` per non-recursive watch each tick and
  adopts or releases from that (0.08 ms/tick on this repo, 0.47 ms on a
  synthetic 300-directory monorepo), then feeds the difference through the
  debouncer so a new directory's existing files are indexed and a deleted
  one's nodes are reconciled away;
- a dead thread whose watch root no longer exists is a release, not a death.
  Both backends stop an emitter when its own root disappears, so `rm -rf lib/`
  used to exit the watcher, and the daemon restarted it every 30s forever;
- the daemon now backs off exponentially between restarts and counts them,
  resetting once a watcher has stayed up;
- the repository root is resolved once, at the `watch()` boundary. `--repo .`
  previously broke adoption and release outright, and — pre-existing — made
  the startup reconciliation treat every stored absolute path as stale and
  delete it. A graph built under a genuinely different root is now refused
  with a clear error rather than reconciled away;
- running out of watch budget degrades to a recursive watch on the parent
  instead of silently covering less, and is reported as `partial` in
  `crg-daemon status`;
- health is published before the first build (a long initial build no longer
  reads as a stall), SIGTERM unwinds like Ctrl+C so `crg-daemon stop` leaves
  no stale health file, the daemon also clears it when it reaps a child, and
  the temp file is named per writer rather than per process;
- `unschedule` is time-boxed like the rest of teardown, since a wedged emitter
  is the premise of this whole change;
- nested build-output exclusions are logged at info level during a build and
  can be spared one at a time with `!path` in `.code-review-graphignore`.

A real-`Observer` regression test covers the new-directory case end to end and
asserts the file reaches the graph; it fails on the previous implementation and
passes now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fHfGDiZedoxjpKzanHri3
T
Tirth Kanani committed
0442a67f5fff19c20acbfabc5c32e7b72a555929
Parent: 1a07617