- Board renamed docs/plan/00-kanban.md -> docs/00-status.md and rebuilt: ▶ NEXT PLAN pointer (iteration 4 — emitter, corpus, `woc build`) then six buckets — stories, in progress, done, pending, discarded, learnings. It covered only the Rust runtime before, so the whole OOP track was invisible. All 16 inbound refs repointed; `Kanban:` banners renamed to `Status:`. - New discarded.md (settled rejections with reasons: inheritance, `abstract`, Money/SKU/Float, Dynamic/cast/macro/extern, AOT-to-C, Menhir, shared engine state) and learnings.md (plumbed≠enforced, vacuous goldens, exit-0-wrong- output, malloc-path ASan trick, deferred checks that never reach the VM). - RECOVERED docs/plan/exploration/blue-green-vm/00-vision.md — gone from disk, never committed (gitignored path), cited by five docs incl. principle 12. Root cause was broader: all seven forward-roadmap plans in docs/superpowers/plans/ were untracked and ignored, on one disk only. Dropped the docs ignore rules with a do-not-re-add note; added __pycache__/*.pyc. - Repaired broken links across docs/, 270 -> 36: fixes a regression from the earlier reference/ -> .dev/reference/ move (relative paths at ../../ and deeper were skipped), plus depth and reorg drift. The 36 residual point at content that does not exist and need decisions, not paths. - New spec docs/superpowers/specs/2026-08-10-logwatcher-gap-closure-design.md, applied: `and`/`or` verdict row; Part 3 gains `env` (six modules), swaps time.mono for iso/local, adds 22 bare core builtins; throw/time.mono/is cut (0 uses in the sample). Plan 8: Task 2 gains and/or, Task 5 drops throw, abstract+`is` task deleted, 8/9 renumber to 7/8. Plan 9 gains core builtins. Plan 10 gains the 307 -> 0 diagnostic gate. WO-E205 re-filed unreachable-by- design. types.ml header drops its false satisfaction-set claim. 00-code- review.md reduced to a stub — its rival Phase 1-4 roadmap retired.
4.8 KiB
05 — inotify
Filesystem event notifications as a file descriptor. inotify_add_watch(dir, mask) installs a watch; reading the fd returns variable-length inotify_event records each time a matching file creates, modifies, or disappears. Replaces the S3 + Lambda pipeline from the v1 architecture — one fd on the event loop IS the content-sync engine.
Kernel source
| Path | What |
|---|---|
reference/linux/fs/notify/inotify/inotify_user.c |
SYSCALL_DEFINE1(inotify_init1, ...), SYSCALL_DEFINE3(inotify_add_watch, ...), SYSCALL_DEFINE2(inotify_rm_watch, ...). |
reference/linux/fs/notify/inotify/inotify_fsnotify.c |
The fsnotify backend that feeds events into the fd. |
reference/linux/include/uapi/linux/inotify.h |
struct inotify_event, IN_* masks. |
Man pages
man 7 inotify (overview + event semantics), man 2 inotify_init1, man 2 inotify_add_watch, man 2 inotify_rm_watch.
Rust FFI via libc
use libc::{inotify_init1, inotify_add_watch, inotify_rm_watch, inotify_event};
use libc::{IN_CLOEXEC, IN_NONBLOCK};
use libc::{IN_MODIFY, IN_CREATE, IN_DELETE, IN_CLOSE_WRITE};
use libc::{IN_MOVED_FROM, IN_MOVED_TO, IN_ISDIR, IN_Q_OVERFLOW};
Direct-syscall example
unsafe {
let fd = libc::inotify_init1(libc::IN_CLOEXEC | libc::IN_NONBLOCK);
if fd < 0 { return Err(io::Error::last_os_error()); }
let dir = std::ffi::CString::new("docs/examples/blog/types").unwrap();
let wd = libc::inotify_add_watch(
fd,
dir.as_ptr(),
(libc::IN_MODIFY | libc::IN_CLOSE_WRITE
| libc::IN_CREATE | libc::IN_DELETE
| libc::IN_MOVED_FROM | libc::IN_MOVED_TO) as u32,
);
if wd < 0 { return Err(io::Error::last_os_error()); }
// register fd on epoll. On EPOLLIN:
let mut buf = [0u8; 4096];
let n = libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len());
// parse the buffer: a packed sequence of inotify_event records,
// each followed by a variable-length name field (event.len bytes)
let mut offset = 0usize;
while offset < n as usize {
let ev = &*(buf.as_ptr().add(offset) as *const inotify_event);
let name_len = ev.len as usize;
let name_start = offset + std::mem::size_of::<inotify_event>();
let name = std::str::from_utf8(&buf[name_start..name_start + name_len])
.unwrap().trim_end_matches('\0');
// dispatch based on ev.mask + ev.wd → directory → full path
offset = name_start + name_len;
}
}
Key flags
| Flag | Meaning |
|---|---|
IN_CLOEXEC / IN_NONBLOCK |
Close on exec, non-blocking reads. Always set. |
IN_MODIFY |
File content written. Fires per-write(2) — noisy; prefer IN_CLOSE_WRITE. |
IN_CLOSE_WRITE |
File opened for writing was closed. Usual choice — one event per editor save. |
IN_CREATE / IN_DELETE |
Entry created / deleted inside a watched directory. |
IN_MOVED_FROM / IN_MOVED_TO |
The two halves of a rename. Paired by cookie. Editors often write tmp → rename → delete; you get both halves. |
IN_ISDIR |
Set on the event when the target is a directory. |
IN_Q_OVERFLOW |
Kernel event queue overflowed; wd = -1, rescan from scratch. Must handle. |
Gotchas
- Per-directory watches, not per-file. Watching individual files wastes descriptors and misses
IN_CREATE/IN_DELETEfor new entries. Watch the directory; filter by eventnamein userspace. - Recursive watching is manual. Walk the tree at init and add a watch per directory. React to
IN_CREATE | IN_ISDIRby adding a watch for the new subdirectory — and toIN_MOVED_TO | IN_ISDIRtoo. fs.inotify.max_user_watchesdefaults to 8192 on most distros. Recursive watches over a big node_modules or target dir exhaust it fast. Filter aggressively before adding.- Editors burst events. Tmp-file + rename + delete is 3–4 events per logical save. Debounce 100–200 ms with
timerfd. - Reading less than a full event is an
EINVAL. Use a buffer ≥sizeof(inotify_event) + NAME_MAX + 1(≈ 4 KiB is a safe size). wdis stable per-watch but reused afterrm_watch. Keep awd → pathmap; remove from it onIN_IGNORED.
Used by
07-inotify-content-watcher.md — the Stage-3 hot-reload feature. Future sub crate — the register-macro subscription model in 00-linux.md § Database Subscription.
v1 port source
reference/crates/wo-watch/src/lib.rs (280 LOC) — already does recursive watch setup, event parsing, and path resolution via a wd → PathBuf map.