I read this as an investigation task - locate the commit observation code, the failing test, and determine exactly how inotify watches are budgeted per invocation, whether they leak, and the smallest event-driven design that fits under 108.

Let me launch parallel exploration


I have full tracing. Here is the root-cause report.

<analysis>
**Literal Request**: Count the inotify watches one `source_golden_path` invocation requests, identify the watched directories, check for leaks, and derive the minimal event-driven design under a budget below 108.
**Actual Need**: Determine which of three hypotheses (over-watch of irrelevant dirs, leak, or unavoidable minimum) explains the failing test, so they can fix it without polling or killing unrelated processes.
**Success Looks Like**: An exact per-invocation watch count, the watched path, a leak verdict, and a one-liner RED seam that pins the minimum.
</analysis>

## Root-cause report

### 1. Watch accounting — one invocation requests exactly 1 inotify watch

The failing test (`tests/gateway/test_source_golden_path_verifier.py:665`) runs `scripts/source_golden_path.py --source ... --evidence-root <bundle>` as a subprocess.

That driver arms exactly one observer with exactly one watched path:

- `scripts/source_golden_path.py:374-375`
  ```
  with ProductionCommitObserver((profile,), receipts) as observer:
      observer.arm(..., watched_paths=(state_dir / "ledger.json",), ...)
  ```
- `state_dir` = `room_bootstrap_state_dir(profile)` = `profile / data/onboarding/telegram-customer-bootstrap-v1` (`gateway/platforms/telegram_customer_bootstrap.py:23-24,37-39`).

Inside `ProductionCommitObserver.arm` (`gateway/commit_observer.py:53-95`):

- Creates one inotify fd via `inotify_init1(O_CLOEXEC|O_NONBLOCK)` at line 73.
- Loops over `watched_paths` and calls `inotify_add_watch(self._fd, path.parent, mask)` per path (lines 78-81).

Since `watched_paths` has exactly one entry, the loop executes once: **1 inotify watch, 1 inotify fd, on the single directory that already exists at `<bundle>/data/onboarding/telegram-customer-bootstrap-v1`** (the parent of `ledger.json`, the thing being committed).

`observe()` at lines 377-383 is called 4 times but never rearms (arm guards `duplicate_watcher_identity` at line 59), so the count stays at 1 for the whole invocation.

### 2. Which directories are watched

Only the parent of the single watched file. No recursion, no `rglob`, no watch on the immutable source tree (`/home/cube/.cache/task26-strict-successor-1786976146/src-p`), no watch on the `0o700` profile root other than the one bootstrap state dir. The authoritative `_roots` (`(profile,)`) is validated (symlink/priv checks, commitment_observer.py:64,72) but never watched itself.

### 3. Leak verdict — no leak is possible

- `close()` (`gateway/commit_observer.py:50-52`) does `os.close(self._fd)`, releasing the watch; the `with` block calls it via `__exit__` (lines 45-48), and the arm failure path closes on `BaseException` (lines 93-95).
- The fd is `O_CLOEXEC`, so nothing propagates to a grandchild.
- The driver is a subprocess; when it exits, the kernel removes every inotify watch owned by that proProcess and closes the fd. inotify watches cannot outlive their arming fd or process.
- The verifier subprocess (`verify_source_golden_path.py`) creates no inotify resources; the `thaw_owned_tmp_tree` fixture only chmods files, and `tests/conftest.py:773` only mentions inotify in a comment. `test_commit_observer.py` builds/destroys observers in-process with a bounded with-block too.

So there is no mechanism for this observer's watch to leak across tests or processes.

### 4. Hypothesis evaluation

- **Recursive over-watch of immutable/unrelated directories — REFUTED.** `arm` watches only `path.parent` per submitted path; no recursion, no whole-profile/source-tree watching.
- **Test cleanup / watch leak — REFUTED.** 1 fd, closed by context-exit and by process exit; inotify is process-scoped; `O_CLOEXEC` contained.
- **Unavoidable minimum watch set — CONFIRMED.** The design already requests exactly 1 watch for the single commit target. That is the theoretical floor for any non-polling, event-driven design (0 watches means polling, which is forbidden). Under any remaining budget >= 1, including budget well below 108, this invocation succeeds.

### 5. Why it can fail "below 108"

The observer's own footprint is 1; it is not the consumer that exhausts a ~108 budget. A real `inotify_watch_failed`/`inotify_init_failed` (lines 75, 80) surfaces only when the box-wide `fs.inotify.max_user_watches` is saturated by unrelated living processes (editors, file watchers, other gateways). The correct action is neither to shrink this observer (it is already at the floor) nor to kill unrelated processes — it is to add the one watch when budget is available, exactly as coded.

### Minimal RED test seam (no edits made)

Smallest seam that pins the invariant — currently green, goes red the moment someone adds over-watch or reintroduces a leak:

- In `tests/gateway/test_commit_observer.py`, replace the ctypes `_LIBC.inotify_add_watch` with a counting wrapper before `_arm()`, then assert the call count equals `len(watched_paths)` (==1) and matches the one `watcher_id` in the receipts.
- Assert no-leak by recording the process fd count (e.g. `len(os.listdir("/proc/self/fd"))`) before `arm()` and after `close()`; it must return to baseline.
- Wire the arm into the driver-visible surface (`source_golden_path.py:375`) so a whole-invocation run asserts the `subscription_armed` receipt carries exactly one `watched_paths` entry.

Both are single-assertion tests; they fail only on a real regression, never by timing, and require no polling or process termination.

<results>
<files>
- /home/cube/projects/richard/hermes-agent/scripts/source_golden_path.py - the driver invoked by the failing test; arms one observer with one watched path (lines 316, 374-383), the sole production caller of `ProductionCommitObserver`.
- /home/cube/projects/richard/hermes-agent/gateway/commit_observer.py - the commit-observation code; counts inotify watches exactly equal to `len(watched_paths)` (1 here): `inotify_init1` line 73, `inotify_add_watch` line 78, watch-name mapping line 81; `close()` lines 50-52 releases the watch.
- /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_customer_bootstrap.py - `room_bootstrap_state_dir` (lines 37-39) and `ROOM_BOOTSTRAP_RELATIVE_PATH` (lines 23-24) define the single watched directory `data/onboarding/telegram-customer-bootstrap-v1`.
- /home/cube/projects/richard/hermes-agent/gateway/_commit_receipts.py - append-only hash-chain receipt log; `subscription_armed`/`commit_observed` receipts record exactly the one watched path and enforce no-duplicate arm.
- /home/cube/projects/richard/hermes-agent/tests/gateway/test_source_golden_path_verifier.py - the failing end-to-end test (test starts line 665); runs the driver subprocess twice plus the verifier, and freezes the bundle (lines 727-730).
- /home/cube/projects/richard/hermes-agent/tests/gateway/test_commit_observer.py - unit coverage of the observer contract (`_observer` line 25, `_arm` line 32, `test_inotify_overflow_is_terminal` line 157); the natural home for the RED seam.
- /home/cube/projects/richard/hermes-agent/scripts/verify_source_golden_path.py - the verifier invoked by the test; creates no inotify resources (PHASES at line 58, no watcher code).
</files>

<answer>
Each invocation of `source_golden_path.py` (the surface the failing test drives) requests exactly **1 inotify watch on 1 directory**: `<bundle>/data/onboarding/telegram-customer-bootstrap-v1/` (the parent of `ledger.json`), on a single `O_CLOEXEC|O_NONBLOCK` inotify fd. There is no recursion, no watch on the immutable source tree, and no additional watch for the profile root. The watch cannot leak across tests or processes: it is closed by the `with`-block `close()`, it is `O_CLOEXEC`, and the driver is a subprocess whose exit releases all its inotify watches. Therefore the correct hypothesis is the unavoidable minimum watch set — 1 is the floor for any event-driven (non-polling) design, and 1 is what the code already uses. A failure "under 108" is environmental inotify saturation by unrelated living processes; the observer itself consumes exactly 1 and should not be reduced further nor should unrelated processes be killed.
</answer>

<next_steps>
No follow-up needed. If the test is failing on a real run, the remaining unknown is box-wide inotify pressure at arm-time (`commit_observer.py:75,80` raising `inotify_init_failed`/`inotify_watch_failed`) — confirm by checking `fs.inotify.max_user_watches` vs live fd/watch counts of unrelated processes before touching code. Otherwise, add the two-assertion RED seam in `tests/gateway/test_commit_observer.py` (watch-call count == len(watched_paths); fd count returns to baseline after close) to keep the minimum pinned.
</next_steps>
</results>