Skip to content

Deepen mdo C2: watch mode becomes a debounce module - #49

Merged
maphew merged 1 commit into
mainfrom
agent/6w5-c2-watch-module
Oct 7, 2026
Merged

maphew merged 1 commit into
mainfrom
agent/6w5-c2-watch-module

Conversation

@maphew

@maphew maphew commented Oct 7, 2026

Copy link
Copy Markdown
Owner

C2: Deepen watch mode into a debounce module

Beads mdo-6w5.3 · parent epic mdo-6w5 (PR order 3 of 6; stacked on the C6 branch, which stacks on C1)

What

New `src/watch.rs` (lib crate) owning all three watch-mode mechanics behind one interface:

```rust
watch::run(input, render) -> notify::Result<()>
```

  • Registers the parent-directory watch before rendering (a save landing during the initial render is queued, not lost), runs `render` once immediately, then once per settled burst (trailing-edge debounce).
  • A render-callback `Err` (transient filesystem state mid-rename) never ends watch mode; channel death ends it gracefully with `Ok(())`.
  • Watch-mode UX lines (`👀 Watching…` / `🔁 File changed…` / `⚠️ Watcher error`) belong to the module; rendering + reporting stay CLI-side via the callback.
  • Injectable timing: debounce + loop clock are pump parameters. Unit tests script a fake event channel against a virtual clock, covering — cross-platform by construction — burst collapse, bursts running longer than the window (trailing event never dropped), events after the window, sibling noise, watcher error storms, channel death mid-drain, callback errors, and the relevance gates (name/parent/Access).

`main.rs` keeps CLI-local helpers (`render_and_report`, `launch_open_output`) and exit-code policy; the `--watch --open` launch hook remains a one-time duty consumed exactly on the first callback (pre-module behavior preserved, byte-compatible print order included).

Coverage change

  • `tests/cli.rs` keeps one spawned-binary e2e smoke: `watch_survives_atomic_rename`.
  • `watch_ignores_sibling_changes` moved into module unit tests (12 new).
  • Byte-identity harness (C1's) still passes for the once-mode CLI surface.

Gates

fmt --check ✓ · clippy -D warnings ✓ · `cargo test --all-targets --all-features --locked` ✓ (101 tests: 66 lib + 21 cli + 17 others)

Signed: kilo-kilo-auto-efficient on behalf of matt (Beads mdo-6w5.3)

C2 / Beads mdo-6w5.3 (epic mdo-6w5, PR order 3 of 6; stacked on the C6
branch).

Watch mode's mechanics were interleaved with main.rs's CLI plumbing:
parent-dir registration rationale, event relevance, the trailing-edge
debounce drain, channel death, and the render-failure continuation all
lived inline around ~180 lines with only two spawned-binary e2e tests as
coverage, both unix-gated.

New src/watch.rs in the lib crate owns all three mechanics behind one
interface:

- watch::run(input, render) - one deep watch entry point. Registers the
  watch BEFORE rendering (a save landing during the initial render is
  queued, not lost), runs the render callback once immediately, then once
  per settled burst. A render callback Err (transient filesystem state
  mid-rename) never ends watch mode; channel death ends it gracefully
  with Ok(()).
- The debounce timing and the loop clock are injectable at pump level:
  unit tests script a fake event channel (relative arrival times) against
  a virtual clock, so the semantics are covered cross-platform by
  construction: burst collapse, bursts running longer than the window,
  events after the window, sibling noise, watcher error storms, channel
  death during a drain, callback errors, and event-relevance gates.
- watch-mode UX lines (Watching / File changed, re-rendering / Watcher
  error) are the module's; rendering and its reporting stay with the CLI
  via the callback.

main.rs keeps the CLI-local helpers (render_and_report, launch_open_-
output) and its exit-code policy; the -watch -open launch hook stays a
one-time duty consumed exactly on the first callback, as before the
module existed.

tests/cli.rs keeps one spawned-binary e2e smoke test
(watch_survives_atomic_rename); the watch_ignores_sibling_changes e2e
moves into the module unit tests.

Gates: fmt --check, clippy --all-targets --all-features -D warnings, and
cargo test --all-targets --all-features --locked all green (12 new watch
unit tests; byte-identity harness against the pre-C1 binary still passes
for the once-mode CLI surface).

Beads-Ref: mdo-6w5.3

Agent-Signature: kilo-kilo-auto-efficient on behalf of matt
Comment thread tests/cli.rs
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

use mdo_cli::temp_output_for;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: temp_output_for import is unused on non-Linux targets

All four call sites (lines 573, 657, 708, 752) sit inside #[cfg(target_os = "linux")] test functions, but this top-level import carries no gate, so macOS and Windows builds of the test target emit an unused_imports warning (and cargo clippy --all-targets -D warnings fails there). The file's own convention gates such imports (#[cfg(unix)] use std::time::{Duration, Instant}; at line 8, #[cfg(target_os = "linux")] use std::os::unix::fs::{symlink, PermissionsExt}; at line 11) — apply the same gate here.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/watch.rs
}

#[test]
fn channel_death_during_drain_renders_and_exits() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Test name/comment over-claim: channel death during the drain is never exercised

Traced with the harness: recv() delivers the modify and opens the window; the drain then polls recv_timeout against an empty script, and FakeRx::now_or_elapsed (lines 288-305) can only return RecvTimeoutError::Timeout — it never constructs Disconnected — so the drain exits via the timeout arm of Err(_) => break (line 166), and death is only seen on the following outer recv(). The pending burst renders because the window timed out, not because channel death broke the drain, and the Disconnected-during-drain path is inexpressible with this harness. Production behavior is identical either way (both variants take the same break), so this is naming/coverage, not a product bug: consider renaming it (e.g. quiet_drain_renders_then_death_ends_watch) or teaching FakeRx a per-entry disconnect mode for recv_timeout.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/main.rs
);
if pending_open_launch {
// The launch hook is a one-time post-initial-render duty
// (the pump runs the callback immediately): consume the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: The immediate callback run belongs to watch::run, not the pump

watch::run invokes the callback once immediately (src/watch.rs:92) before it ever calls pump (src/watch.rs:99); pump only runs the callback for post-window re-renders (src/watch.rs:175). Pointing at the pump could send a reader hunting inside it for the initial launch.

Suggested change
// (the pump runs the callback immediately): consume the
// (watch::run runs the callback immediately): consume the

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/main.rs
}
pending_open_launch = false;
}
result.map(|_| ())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: No-op map — result is already Result<(), RenderError>

render_and_report returns Result<(), RenderError> (main.rs:474), so .map(|_| ()) maps Ok(()) to Ok(()). Unlike outcome.map(|_| ()) inside render_and_report (main.rs:484), which legitimately erases a ConvertOutcome, this erases nothing and just reads like it does.

Suggested change
result.map(|_| ())
result

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/main.rs
private_output,
css_override,
});
report_render(input, output, &outcome, verbose);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: report_render's success bool is discarded; its doc's exit-code justification is now stale

This is report_render's only remaining call site, and nothing consumes the returned bool — the exit-code signal now comes from render_and_report(...).is_ok() (main.rs:441). The helper's doc (main.rs:521-522, unchanged by this PR) still reads "Returns whether the render succeeded, so exit-code policy stays local to main", which no longer holds. Consider dropping the bool return or refreshing the doc.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/watch.rs

/// Fake channel half driving `pump` from a script. Events ready within
/// the polled timeout are delivered on that poll; anything later goes
/// past the window (timed out), exactly as a real channel would.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: FakeRx re-charges a queued item's ready_after from zero after a Timeout

When now_or_elapsed times out it advances the clock by the whole timeout but leaves the front item with its original relative ready_after, so the next poll/recv re-charges the full wait, double-counting time the Timeout already consumed (a real channel would deliver a queued item at its absolute readiness time). No current test verdict depends on this (traced), but the "exactly as a real channel would" phrasing overreaches, and any future assertion touching virtual time could be silently off.

Suggested change
/// past the window (timed out), exactly as a real channel would.
/// past the window (timed out).

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 4
Issue Details (click to expand)

WARNING

File Line Issue
src/watch.rs 459 channel_death_during_drain_renders_and_exits never exercises death during the drain: FakeRx's recv_timeout can only return Timeout (no Disconnected path exists in the harness), so the drain exits via timeout and the test name/comment over-claim

SUGGESTION

File Line Issue
src/main.rs 420 Comment attributes the immediate callback run to "the pump"; watch::run performs it (src/watch.rs:92) before pump starts
src/main.rs 428 `result.map(
src/main.rs 483 report_render's returned bool is discarded at its only call site; its doc (main.rs:521-522, unchanged lines) still claims it drives the exit-code policy
src/watch.rs 281 FakeRx doc "exactly as a real channel would" overreaches: after a Timeout a queued item's relative ready_after is re-charged in full (elapsed wait double-counted); today's test verdicts are unaffected
Files Reviewed (7 files)
  • tests/cli.rs - previous un-gated temp_output_for import RESOLVED by the incremental commit; verified the new #[cfg(target_os = "linux")] gate matches all 4 call sites (lines 574, 658, 709, 753), each inside a linux-gated test, and the symbol is exported unconditionally (src/lib.rs:143)
  • docs/metrics/history/2026-10-07.json - generated metrics snapshot, no issues
  • docs/metrics/latest.json - generated metrics data, no issues
  • docs/metrics/index.html - generated metrics page; values match latest.json/history snapshot, no issues
  • src/main.rs - 3 issues (carried forward, re-verified present at HEAD 5e07917)
  • src/watch.rs - 2 issues (carried forward, re-verified present at HEAD 5e07917)
  • src/lib.rs - targeted check only (unchanged by incremental diff), no issues

Fix these issues in Kilo Cloud

Notes: Incremental review since f59f85c: generated metrics artifacts (history snapshot, latest.json, index.html) are internally consistent, and the single code change gates the temp_output_for test import exactly as previously recommended — that WARNING is resolved. The 5 remaining findings are unchanged-comment/docs items re-verified against current HEAD; they are already covered by active inline comments, so no new inline comments were posted. Note this PR is now MERGED; findings are advisory follow-ups. Rust toolchain remains unavailable in the review container, so gates were assessed statically.

Previous Review Summary (commit f59f85c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit f59f85c)

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 4
Issue Details (click to expand)

WARNING

File Line Issue
tests/cli.rs 6 use mdo_cli::temp_output_for; is un-gated but all four call sites (lines 573, 657, 708, 752) are inside #[cfg(target_os = "linux")] tests — unused-import warning on macOS/Windows test builds; gate it like the file's other platform imports (lines 8, 11)
src/watch.rs 459 channel_death_during_drain_renders_and_exits never exercises death during the drain: FakeRx's recv_timeout can only return Timeout (no Disconnected path exists in the harness), so the drain exits via timeout and the test name/comment over-claim

SUGGESTION

File Line Issue
src/main.rs 420 Comment attributes the immediate callback run to "the pump"; watch::run performs it (src/watch.rs:92) before pump starts
src/main.rs 428 `result.map(
src/main.rs 483 report_render's returned bool is discarded at its only call site; its doc (main.rs:521-522, unchanged lines) still claims it drives the exit-code policy
src/watch.rs 281 FakeRx doc "exactly as a real channel would" overreaches: after a Timeout a queued item's relative ready_after is re-charged in full (elapsed wait double-counted); today's test verdicts are unaffected
Files Reviewed (4 files)
  • src/lib.rs - no issues (module doc + pub mod watch; verified against the code)
  • src/main.rs - 3 issues
  • src/watch.rs - 2 issues
  • tests/cli.rs - 1 issue

Notes: The refactor was verified as behavior-preserving against base e3290fb: register-before-render ordering, print order (✅ Converted → 🌐/❌ launch → 👀 Watching; 🔁 File changed printed before re-render output), one-shot exit-code policy, one-time --open launch consumption (including first-render-failure), debounce math, relevance gates, and channel-death handling are line-equivalent to the pre-module inline implementation. All 12 new watch.rs unit tests were traced against the real pump and every assertion holds for the reason its comment claims (no false passes found). notify API usage matches the locked notify 6.1.1. No inline comments existed before this review, so none are duplicates. Verification caveat: Rust toolchain is unavailable in this review container, so compile/test gates were assessed statically; the PR reports fmt/clippy/tests green.

Fix these issues in Kilo Cloud


Reviewed by efficient · Input: 48.8K · Output: 8.5K · Cached: 613.1K

@maphew
maphew force-pushed the agent/6w5-c2-watch-module branch from f59f85c to 5e07917 Compare October 7, 2026 17:06
@maphew
maphew merged commit 7625e53 into main Oct 7, 2026
3 of 4 checks passed
@maphew
maphew deleted the agent/6w5-c2-watch-module branch October 7, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant