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