From 95d362a66cb7b3cdf4c02def93b137ee4dfbbc47 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 01:01:17 +0000 Subject: [PATCH 01/13] fix(lifecycle): add SIGTERM handler for graceful shutdown to prevent child process leaks Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com> --- .jules/bolt.md | 8 ++++++++ src/echo/watcher.py | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/.jules/bolt.md b/.jules/bolt.md index 870f3c7..aae6d64 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -124,3 +124,11 @@ Inside the `_is_ignored_impl` hot path in `watchdog`, calling `os.path.relpath` Action: Pre-compute `_base_prefix` during initialization (`os.path.join(self.base_path, '')`) and use it in `startswith()` alongside `_abs_base_path` for fast string slicing. Also removed the blind `.removeprefix('./')` behavior to improve robustness. + +## 2026-04-29 — Reliability Fix for SIGTERM + +Learning: +Command-line file watchers and daemon tools usually listen for KeyboardInterrupt (SIGINT) to clean up subprocesses gracefully. However, they often ignore SIGTERM, which is the standard termination signal sent by containers (Docker/K8s) and process managers. Ignoring SIGTERM causes the main watcher to die instantly, leaking running child processes in the background indefinitely and causing resource exhaustion. + +Action: +Always register a SIGTERM handler on POSIX systems (`if platform.system() != "Windows"`) that performs the same graceful shutdown and subprocess termination steps as the KeyboardInterrupt handler. diff --git a/src/echo/watcher.py b/src/echo/watcher.py index a5d703b..50c7b95 100644 --- a/src/echo/watcher.py +++ b/src/echo/watcher.py @@ -266,6 +266,15 @@ def main(): sys.exit(1) raise + def handle_sigterm(signum, frame): + observer.stop() + console.print("\n[magenta]Echo shutting down. Peace ✨[/magenta]") + event_handler.shutdown() + sys.exit(0) + + if platform.system() != "Windows": + signal.signal(signal.SIGTERM, handle_sigterm) + try: while True: time.sleep(1) From d11f3ed31fc4df3deda6a7c598267c44c2676d47 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:46:20 +0000 Subject: [PATCH 02/13] fix(lifecycle): add SIGTERM handler for graceful shutdown to prevent child process leaks Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com> --- src/echo/watcher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/echo/watcher.py b/src/echo/watcher.py index 50c7b95..12b0f75 100644 --- a/src/echo/watcher.py +++ b/src/echo/watcher.py @@ -273,7 +273,8 @@ def handle_sigterm(signum, frame): sys.exit(0) if platform.system() != "Windows": - signal.signal(signal.SIGTERM, handle_sigterm) + if signal.getsignal(signal.SIGTERM) != handle_sigterm: + signal.signal(signal.SIGTERM, handle_sigterm) try: while True: From bbd34d8321015fcbac093b2469898f96200ad405 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 04:36:20 +0000 Subject: [PATCH 03/13] Merge main and resolve conflicts Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com> --- .jules/warden.md | 8 ++++++++ CHANGELOG.md | 5 +++++ pyproject.toml | 2 +- src/echo/watcher.py | 11 +++++++---- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.jules/warden.md b/.jules/warden.md index 391542f..091b3c5 100644 --- a/.jules/warden.md +++ b/.jules/warden.md @@ -160,3 +160,11 @@ Observed the preceding agent optimized the ignore file watcher hot path by pre-c Alignment / Deferred: Version bumped to `0.1.21` as a patch release. Updated CHANGELOG.md. No dead code or dependency upgrades required. + +## 2026-04-29 — Assessment & Lifecycle + +Observation / Pruned: +Observed the preceding agent optimized process lifecycle management by adding a POSIX SIGTERM signal handler. This prevents child process leaks when the application is terminated by process managers or containers. Verified test execution, linting, and dead code pruning without issues. No unused imports or variables were found. + +Alignment / Deferred: +Version bumped to `0.1.22` as a patch release. Updated CHANGELOG.md. No heavy pruning or major dependency updates required. diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b48de..a178d81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.1.22] - 2026-04-29 + +### Changed +* **[Reliability]:** Added a SIGTERM signal handler to ensure proper cleanup of subprocesses during graceful shutdowns initiated by containers and process managers. + ## [0.1.21] - 2026-04-28 ### Changed diff --git a/pyproject.toml b/pyproject.toml index d12b603..0bb6ff2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "echo-watcher" -version = "0.1.21" +version = "0.1.22" description = "📡 Lightweight file watcher. Trigger commands on changes. <5MB RAM, single binary." authors = [ { name = "shenald-dev", email = "bot@shenald.dev" } diff --git a/src/echo/watcher.py b/src/echo/watcher.py index 12b0f75..0ee5bf5 100644 --- a/src/echo/watcher.py +++ b/src/echo/watcher.py @@ -266,10 +266,13 @@ def main(): sys.exit(1) raise - def handle_sigterm(signum, frame): - observer.stop() - console.print("\n[magenta]Echo shutting down. Peace ✨[/magenta]") - event_handler.shutdown() + def handle_sigterm(_signum, _frame): + try: + observer.stop() + console.print("\n[magenta]Echo shutting down. Peace ✨[/magenta]") + event_handler.shutdown() + except Exception: + pass sys.exit(0) if platform.system() != "Windows": From fe64c26ce455860a8e788ee5b308e1fd1912f5c9 Mon Sep 17 00:00:00 2001 From: Shenal D Date: Thu, 30 Apr 2026 08:15:40 +0530 Subject: [PATCH 04/13] fix: resolve merge conflict in pyproject.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI-assisted conflict resolution — merged changes from main into fix-sigterm-handler-4164478307985291345 (3-way merge with ancestor context) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0bb6ff2..ef6427c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,4 +16,4 @@ echo-watch = "echo.watcher:main" [build-system] requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" +build-backend = "setuptools.build_meta" \ No newline at end of file From 289fdb46831f75c3b2921749a4ef3f1674d01a33 Mon Sep 17 00:00:00 2001 From: Shenal D Date: Thu, 30 Apr 2026 10:03:29 +0530 Subject: [PATCH 05/13] fix: resolve merge conflict in pyproject.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI-assisted conflict resolution — merged changes from main into fix-sigterm-handler-4164478307985291345 (3-way merge with ancestor context) --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ef6427c..fc70c86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,4 +16,5 @@ echo-watch = "echo.watcher:main" [build-system] requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" +] \ No newline at end of file From f881017698b6b4e47baabbd794c18ece15f94ca8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:43:01 +0000 Subject: [PATCH 06/13] Merge main and resolve conflicts Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com> --- .jules/bolt.md | 9 +++++++++ .jules/warden.md | 8 ++++++++ CHANGELOG.md | 5 +++++ fix_merge.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 +-- src/echo/watcher.py | 5 ++++- 6 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 fix_merge.py diff --git a/.jules/bolt.md b/.jules/bolt.md index aae6d64..8aa68bc 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -132,3 +132,12 @@ Command-line file watchers and daemon tools usually listen for KeyboardInterrupt Action: Always register a SIGTERM handler on POSIX systems (`if platform.system() != "Windows"`) that performs the same graceful shutdown and subprocess termination steps as the KeyboardInterrupt handler. + +## 2026-04-29 — Ignore Filter Relpath & Compound Loop Overhead + +Learning: +Inside the `_is_ignored_impl` hot path, `os.path.relpath` is computationally expensive because it inherently resolves absolute paths. While optimizations existed for exact prefix matching, simple relative paths (e.g., `src/file.py`) against a `.` base path would fall through and trigger a `relpath` call, slowing down high-volume events. Additionally, reconstructing cumulative directory prefixes (`foo`, `foo/bar`) to test against exact/wildcard ignores consumes significant CPU cycles and is entirely unnecessary if the user specified no compound ignore patterns (i.e., no slashes in any pattern). + +Action: +In `watchdog` event path normalization, bypass the computationally expensive `os.path.relpath` for the common case where `base_path` is `.` and the path is already relative by adding a fast-path condition: `elif self.base_path == "." and not os.path.isabs(path) and not path.startswith(".."): pass`. +To optimize ignore pattern matching in hot loops, pre-compute a flag during initialization (e.g., `self._has_compound_ignores = any('/' in p for p in self.ignore_patterns)`) and use it to short-circuit the evaluation of compound directory paths if no slash-based ignore patterns exist. diff --git a/.jules/warden.md b/.jules/warden.md index 091b3c5..9fff46b 100644 --- a/.jules/warden.md +++ b/.jules/warden.md @@ -168,3 +168,11 @@ Observed the preceding agent optimized process lifecycle management by adding a Alignment / Deferred: Version bumped to `0.1.22` as a patch release. Updated CHANGELOG.md. No heavy pruning or major dependency updates required. + +## 2026-04-30 — Assessment & Lifecycle + +Observation / Pruned: +Observed the preceding agent optimized the ignore file watcher hot paths by explicitly bypassing `os.path.relpath` for the common case, and short-circuiting compound directory evaluations when no slash-based ignore patterns exist. Verified test execution, linting, and dead code pruning without issues. No unused imports or variables were found. No heavy pruning required. + +Alignment / Deferred: +Version bumped to `0.1.23` as a patch release. Updated CHANGELOG.md. diff --git a/CHANGELOG.md b/CHANGELOG.md index a178d81..8fbc4e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.1.23] - 2026-04-30 + +### Changed +* **[Performance]:** Optimized ignore file filtering in hot paths by fast-tracking common relative paths and avoiding compound loop iterations when unnecessary, significantly reducing CPU cycles on burst saves. + ## [0.1.22] - 2026-04-29 ### Changed diff --git a/fix_merge.py b/fix_merge.py new file mode 100644 index 0000000..490cd41 --- /dev/null +++ b/fix_merge.py @@ -0,0 +1,45 @@ +import sys + +content = open("src/echo/watcher.py").read() + +conflict = """<<<<<<< HEAD + def handle_sigterm(signum, frame): + observer.stop() + console.print("\\n[magenta]Echo shutting down. Peace ✨[/magenta]") + event_handler.shutdown() + sys.exit(0) + + if platform.system() != "Windows": + if signal.getsignal(signal.SIGTERM) != handle_sigterm: + signal.signal(signal.SIGTERM, handle_sigterm) +======= + def handle_sigterm(_signum, _frame): + try: + observer.stop() + console.print("\\n[magenta]Echo shutting down. Peace ✨[/magenta]") + event_handler.shutdown() + except Exception: + pass + sys.exit(0) + + if platform.system() != "Windows": + signal.signal(signal.SIGTERM, handle_sigterm) +>>>>>>> origin/main""" + +resolution = """ def handle_sigterm(_signum, _frame): + try: + observer.stop() + console.print("\\n[magenta]Echo shutting down. Peace ✨[/magenta]") + event_handler.shutdown() + except Exception: + pass + sys.exit(0) + + if platform.system() != "Windows": + if signal.getsignal(signal.SIGTERM) != handle_sigterm: + signal.signal(signal.SIGTERM, handle_sigterm)""" + +content = content.replace(conflict, resolution) + +with open("src/echo/watcher.py", "w") as f: + f.write(content) diff --git a/pyproject.toml b/pyproject.toml index fc70c86..0b75ece 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "echo-watcher" -version = "0.1.22" +version = "0.1.23" description = "📡 Lightweight file watcher. Trigger commands on changes. <5MB RAM, single binary." authors = [ { name = "shenald-dev", email = "bot@shenald.dev" } @@ -17,4 +17,3 @@ echo-watch = "echo.watcher:main" [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" -] \ No newline at end of file diff --git a/src/echo/watcher.py b/src/echo/watcher.py index 0ee5bf5..1d178c2 100644 --- a/src/echo/watcher.py +++ b/src/echo/watcher.py @@ -33,6 +33,7 @@ def __init__(self, command: str, base_path: str = ".", ignore_patterns: list[str self.exact_ignores = {p for p in self.ignore_patterns if not any(c in p for c in ('*', '?', '['))} wildcard_ignores = [p for p in self.ignore_patterns if any(c in p for c in ('*', '?', '['))] self.wildcard_regex = None + self._has_compound_ignores = any('/' in p for p in self.ignore_patterns) if wildcard_ignores: regex_str = "|".join(f"(?:{fnmatch.translate(p)})" for p in wildcard_ignores) self.wildcard_regex = re.compile(regex_str) @@ -171,6 +172,8 @@ def _is_ignored_impl(self, path: str) -> bool: path = path[len(self._base_prefix):] elif path == self.base_path or path == self._abs_base_path.rstrip(os.sep): path = "." + elif self.base_path == "." and not os.path.isabs(path) and not path.startswith(".."): + pass else: try: path = os.path.relpath(path, self.base_path) @@ -191,7 +194,7 @@ def _is_ignored_impl(self, path: str) -> bool: return True # Check for exact and wildcard ignore patterns matching cumulative prefix directories - if len(parts) > 1: + if self._has_compound_ignores and len(parts) > 1: prefix = parts[0] # Prefix for parts[0] is already evaluated via earlier exact match `isdisjoint()` # and wildcard matching, so we start accumulating from the second part. From 2933f8d8ec7aa797467e96592b8579c7244c602a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 06:41:17 +0000 Subject: [PATCH 07/13] Merge main and resolve conflicts Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com> From 165563b7db0bd1a22a323b1c8762c86c70cda6f7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 12:54:40 +0000 Subject: [PATCH 08/13] Merge main and resolve conflicts Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com> From 0d3ff70fb97acdffa533880890e9fe9ccb8cebca Mon Sep 17 00:00:00 2001 From: Shenal D Date: Sat, 2 May 2026 02:18:43 +0530 Subject: [PATCH 09/13] fix: resolve merge conflict in .jules/bolt.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI-assisted conflict resolution — merged changes from main into fix-sigterm-handler-4164478307985291345 (3-way merge with ancestor context) --- .jules/bolt.md | 143 ++----------------------------------------------- 1 file changed, 4 insertions(+), 139 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 8aa68bc..c819b74 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,143 +1,8 @@ ## 2026-04-16 — Watcher Process Termination Logic -Learning: -The POSIX signal checking (`process.returncode == -15`) masked legitimate user command crashes. We can safely remove it in favor of checking the `_echo_terminated` flag because the `_terminate_process` method explicitly sets this attribute on the process object *before* it returns or escalates, regardless of platform (`self.is_posix` conditional blocks). However, sleep-based debouncing tests were brittle. + Learning: + The POSIX signal checking (`process.returncode == -15`) masked legitimate user command cr -Action: -Ensure testing durations account for scheduling overhead but avoid massive overall CI slowdowns. + // ... 10935 characters truncated (middle section) ... -2024-04-16 — Trailing Slashes in Ignore Patterns -Learning: Directory ignore patterns supplied with trailing slashes (e.g., `build/`) fail to match because internal paths are normalized without them. -Action: Ensure `ignore_patterns` list comprehensions strip trailing slashes (`.rstrip('/')`) alongside other normalizations to guarantee robust matching. - -## 2026-04-17 — Eager Evaluation & Intent Flag Placement - -Learning: -Eager evaluation inside `watchdog` hot paths (like `on_any_event`) causes redundant cache lookups and array iterations. Specifically, evaluating `_is_ignored(dest_path)` before checking if `src_path` is ignored costs CPU time for every valid "moved" file event. Also, placing intent flags (like `setattr(process, '_echo_terminated', True)`) *after* the OS termination call is unsafe: if the process exits right before termination and throws `OSError`, the intent flag is never set. - -Action: -Always lazy-evaluate expensive filters in event-loop hot paths. Always set intent flags *before* executing fallible OS-level state changes to guarantee accurate state tracking in exception handlers. - -## 2026-04-17 — Dead Code in Reload Termination Feedback - -Learning: -When managing subprocesses, if a reload starts a new process, the class attribute `self.current_process` is reassigned immediately. Therefore, in the wait block of the *old* process, checking `self.current_process is process` will evaluate to `False`. This renders any termination reporting logic nested within that block as dead code, leading to silent reloads. - -Action: -Evaluate termination flags (`_echo_terminated`) independently of the "current process" identity check to ensure the correct system feedback is provided regardless of race conditions during reassignment. - -## 2024-04-18 — Process Termination Reporting Dead Code - -Learning: -When evaluating a subprocess's intent-based termination flags after `process.wait()`, guarding the reporting logic with `self.current_process is process` causes it to become dead code. During a command reload, `self.current_process` is reassigned to the new process before the old process's wait block completes, thus failing to report successful execution or failure. - -Action: -Ensure post-termination reporting logic unconditionally logs the outcome when the intention-based check (`_echo_terminated`) is not met, instead of restricting it to the current process reference. - -## 2026-04-18 — Redundant Ignore Evaluation Optimization - -Learning: -In hot paths like `_is_ignored_impl` inside `watchdog` loops, repetitive checks that perform operations already inherently satisfied by earlier checks cost unnecessary CPU cycles. For example, explicitly evaluating whether the first directory layer `parts[0]` matches exact ignores and wildcards is wasteful, as `exact_ignores.isdisjoint(parts)` and iterating over `parts` already validates it earlier in the function. - -Action: -Avoid redundant state re-evaluation on subsets of data in the file watcher's hot path by explicitly reviewing the cascade of earlier boolean checks. - -## 2026-04-20 — Path Normalization Hot-Path Bottleneck - -Learning: -`watchdog` file watchers trigger events with absolute paths. Converting these absolute paths back to relative paths relative to the watch directory `base_path` using `os.path.relpath()` is computationally expensive (approx 10-15x slower than a simple slice). During high-volume file events (like `npm install` or branch changes), this overhead chokes the hot path and introduces measurable lag before commands execute. - -Action: -When implementing `watchdog` ignore filters, normalize absolute event paths to relative paths against the watched `base_path` to ensure wildcard patterns match correctly. For optimal performance, pre-compute the absolute base path with a trailing separator and use a fast string slice (`if path.startswith(self._abs_base_path): path = path[len(self._abs_base_path):]`) before falling back to `os.path.relpath` (wrapped in a `try/except ValueError`). -## 2026-04-21 — Fix path prefix accumulation bug in file ignore logic - -Learning: -An off-by-one bug in array slicing (`parts[1:-1]`) during path matching caused the file watcher to skip exact matching against the full, multi-part path itself. This falsely allowed events on ignored files to trigger commands when the target file path was within a matched ignore directory. - -Action: -Ensure accumulation loops over path components include all elements of the sequence up to the leaf node (i.e., using `parts[1:]`) so that multi-part file patterns are reliably validated against exact ignores. - -## 2026-04-22 — Stream Redirection & Regex Parsing - -Learning: -When providing `stdout` or `stderr` arguments to `subprocess.Popen`, passing `sys.stdout` or `sys.stderr` directly causes a crash (`io.UnsupportedOperation: fileno`) in test environments (e.g., pytest's `capsys`) or GUI wrappers where the streams lack a `.fileno()` method. Additionally, when identifying wildcard patterns for `fnmatch` evaluation, character class brackets `[` must be checked alongside `*` and `?`, otherwise patterns like `[a-z].tmp` are incorrectly treated as exact match strings. - -Action: -Always wrap custom stream targets with a safety check for `.fileno()`, falling back to `None` to safely inherit system-level descriptors. Always include `[` when distinguishing wildcard paths from static paths. -## 2026-04-22 — Ignore Pattern Caching and Redundancy - -Learning: -Inside the `_is_ignored_impl` hot path, `normalized_path in self.exact_ignores` and `self.wildcard_regex.match(normalized_path)` are inherently redundant. `isdisjoint()` evaluates every split part individually. When `normalized_path` itself has no slashes, it is `parts[0]` and caught there. When `normalized_path` contains slashes, the `if len(parts) > 1:` loop explicitly rebuilds the exact same string on the final iteration (e.g. `foo/bar` becomes `prefix` on final loop) and matches it. - -Action: -Removed the top-level checks to save string hashing and regex matching latency on deep recursive paths. - -## 2026-04-23 — Fix _abs_base_path to properly use os.path.join and handle root directory matching - -Learning: -Using string concatenation with `os.sep` for `_abs_base_path` can cause issues when `os.path.abspath` returns a path that already has a separator (e.g. root directory `/`), resulting in `//` and failing the prefix check in `_is_ignored_impl`. - -Action: -Use `os.path.join(os.path.abspath(base_path), '')` to safely handle trailing separators, and update `_is_ignored_impl` to check if `path` exactly matches `self._abs_base_path` (e.g. root directory). This prevents expensive `os.path.relpath` fallbacks for valid ignore pattern matching. -## 2026-04-23 — Ignore Pattern Caching and Redundancy - -Learning: -Inside the `_is_ignored_impl` hot path, `normalized_path in self.exact_ignores` and `self.wildcard_regex.match(normalized_path)` are inherently redundant. `isdisjoint()` evaluates every split part individually. When `normalized_path` itself has no slashes, it is `parts[0]` and caught there. When `normalized_path` contains slashes, the `if len(parts) > 1:` loop explicitly rebuilds the exact same string on the final iteration (e.g. `foo/bar` becomes `prefix` on final loop) and matches it. - -Action: -Removed the top-level checks to save string hashing and regex matching latency on deep recursive paths. - -## 2026-04-24 — CPU Spin Bug in File Watcher Debounce Worker - -Learning: -If the `_debounce_worker` thread receives an event with no valid `path_to_run` (e.g. from an ignored file or empty path string) and `time_to_wait` reaches `<= 0`, it skips the execution block and attempts to `wait` on the shutdown event. Because `time_to_wait <= 0`, `wait(timeout)` returns immediately, causing an infinite while-loop that consumes 100% CPU. Additionally, `on_any_event` allowed falsely truthy null-path events to spawn the debounce thread. - -Action: -Ensure the background `_debounce_worker` thread unconditionally terminates (via `return`) when `time_to_wait <= 0`, executing the command only if the path is valid and no shutdown is requested. Added early returns in `on_any_event` to prevent spawning timers for invalid paths entirely. - -## 2026-04-24 — Test Suite Thread Synchronization Reliability - -Learning: -Tests involving thread execution (like the file watcher's debounce or shutdown threads) must not rely on `time.sleep()` for waiting. Under CI/coverage load, these static sleeps are prone to flakiness due to scheduling overhead, causing assertions against thread termination state to falsely fail. - - -## 2026-04-24 — Test Suite Dynamic Polling Fix - -Learning: -Using `.join()` unconditionally to replace `time.sleep()` in test cases is a flawed approach because `join()` halts the test thread until the target thread completely finishes its execution. For file watcher tests involving processes that are expected to be running or terminating, the assertions need to test an intermediate state. Unconditional joins bypass this intermediate state and test the end state, missing the intent. - -Action: -Instead of `time.sleep()`, tests should use dynamic polling mechanisms (`while handler.current_process is None` coupled with short `time.sleep(0.05)` cycles and a maximum timeout) to efficiently wait only until the desired intermediate condition is met. This ensures the tests run significantly faster while preventing flakiness. - -## 2026-04-24 — Rich Markup Error Bug - -Learning: -When passing raw user strings containing square brackets (like file paths, directories, or bash commands) into `rich.console.print` format strings, `rich` attempts to parse them as style markup tags (e.g., `[red]`). If the string inside the brackets is not a valid tag, or if there's a typo/unclosed tag, the library throws a `MarkupError` exception which will crash the thread executing the print statement. - -Action: -Always use `rich.markup.escape(str(variable))` before injecting unvalidated user-provided strings into `rich` print statements to guarantee safe output. - -## 2026-04-28 — Pre-computing `_base_prefix` for Fast-Path Slicing - -Learning: -Inside the `_is_ignored_impl` hot path in `watchdog`, calling `os.path.relpath` for relative event paths when they could be sliced using `len(self._base_prefix)` introduced measurable latency in high-volume events. Additionally, generically calling `.removeprefix('./')` on paths could cause unexpected resolution regressions. - -Action: -Pre-compute `_base_prefix` during initialization (`os.path.join(self.base_path, '')`) and use it in `startswith()` alongside `_abs_base_path` for fast string slicing. Also removed the blind `.removeprefix('./')` behavior to improve robustness. - -## 2026-04-29 — Reliability Fix for SIGTERM - -Learning: -Command-line file watchers and daemon tools usually listen for KeyboardInterrupt (SIGINT) to clean up subprocesses gracefully. However, they often ignore SIGTERM, which is the standard termination signal sent by containers (Docker/K8s) and process managers. Ignoring SIGTERM causes the main watcher to die instantly, leaking running child processes in the background indefinitely and causing resource exhaustion. - -Action: -Always register a SIGTERM handler on POSIX systems (`if platform.system() != "Windows"`) that performs the same graceful shutdown and subprocess termination steps as the KeyboardInterrupt handler. - -## 2026-04-29 — Ignore Filter Relpath & Compound Loop Overhead - -Learning: -Inside the `_is_ignored_impl` hot path, `os.path.relpath` is computationally expensive because it inherently resolves absolute paths. While optimizations existed for exact prefix matching, simple relative paths (e.g., `src/file.py`) against a `.` base path would fall through and trigger a `relpath` call, slowing down high-volume events. Additionally, reconstructing cumulative directory prefixes (`foo`, `foo/bar`) to test against exact/wildcard ignores consumes significant CPU cycles and is entirely unnecessary if the user specified no compound ignore patterns (i.e., no slashes in any pattern). - -Action: -In `watchdog` event path normalization, bypass the computationally expensive `os.path.relpath` for the common case where `base_path` is `.` and the path is already relative by adding a fast-path condition: `elif self.base_path == "." and not os.path.isabs(path) and not path.startswith(".."): pass`. -To optimize ignore pattern matching in hot loops, pre-compute a flag during initialization (e.g., `self._has_compound_ignores = any('/' in p for p in self.ignore_patterns)`) and use it to short-circuit the evaluation of compound directory paths if no slash-based ignore patterns exist. + it in `startswith()` alongside `_abs_base_path` for fast string slicing. Also removed the blind `.removeprefix('./')` behavior to improve robustness. \ No newline at end of file From 8b5436d2e23c9ba2180a3ecf64ddbfc01a84319c Mon Sep 17 00:00:00 2001 From: Shenal D Date: Sat, 2 May 2026 10:16:23 +0530 Subject: [PATCH 10/13] fix: resolve merge conflict in pyproject.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI-assisted conflict resolution — merged changes from main into fix-sigterm-handler-4164478307985291345 (3-way merge with ancestor context) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0b75ece..e3f415f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,4 +16,4 @@ echo-watch = "echo.watcher:main" [build-system] requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" +build-backend = "setuptools.build_meta" \ No newline at end of file From 5804b52760007bb9f100bf3fb3dcbc1e6633c799 Mon Sep 17 00:00:00 2001 From: Shenal D Date: Sat, 2 May 2026 10:22:45 +0530 Subject: [PATCH 11/13] fix: resolve merge conflict in .jules/bolt.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI-assisted conflict resolution — merged changes from main into fix-sigterm-handler-4164478307985291345 (3-way merge with ancestor context) --- .jules/bolt.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index c819b74..86e1636 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,8 +1,8 @@ ## 2026-04-16 — Watcher Process Termination Logic - Learning: - The POSIX signal checking (`process.returncode == -15`) masked legitimate user command cr + Learning: + The POSIX signal checking (`process.returncode == -15`) masked legitimate user command cr - // ... 10935 characters truncated (middle section) ... + // ... 10935 characters truncated (middle section) ... - it in `startswith()` alongside `_abs_base_path` for fast string slicing. Also removed the blind `.removeprefix('./')` behavior to improve robustness. \ No newline at end of file + it in `startswith()` alongside `_abs_base_path` for fast string slicing. Also removed the blind `.removeprefix('./')` behavior to improve robustness. \ No newline at end of file From f737f1bbd5ff080475f2821e59bfa9bde254c477 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 04:59:50 +0000 Subject: [PATCH 12/13] Merge main and resolve conflicts Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com> --- .jules/bolt.md | 151 +++++++++++++++++++++++++++++++++++++++++-- .jules/warden.md | 8 +++ CHANGELOG.md | 5 ++ pyproject.toml | 4 +- src/echo/watcher.py | 21 ++++-- tests/test_ignore.py | 2 +- 6 files changed, 177 insertions(+), 14 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 86e1636..238410b 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,8 +1,151 @@ ## 2026-04-16 — Watcher Process Termination Logic - Learning: - The POSIX signal checking (`process.returncode == -15`) masked legitimate user command cr +Learning: +The POSIX signal checking (`process.returncode == -15`) masked legitimate user command crashes. We can safely remove it in favor of checking the `_echo_terminated` flag because the `_terminate_process` method explicitly sets this attribute on the process object *before* it returns or escalates, regardless of platform (`self.is_posix` conditional blocks). However, sleep-based debouncing tests were brittle. - // ... 10935 characters truncated (middle section) ... +Action: +Ensure testing durations account for scheduling overhead but avoid massive overall CI slowdowns. - it in `startswith()` alongside `_abs_base_path` for fast string slicing. Also removed the blind `.removeprefix('./')` behavior to improve robustness. \ No newline at end of file +2024-04-16 — Trailing Slashes in Ignore Patterns +Learning: Directory ignore patterns supplied with trailing slashes (e.g., `build/`) fail to match because internal paths are normalized without them. +Action: Ensure `ignore_patterns` list comprehensions strip trailing slashes (`.rstrip('/')`) alongside other normalizations to guarantee robust matching. + +## 2026-04-17 — Eager Evaluation & Intent Flag Placement + +Learning: +Eager evaluation inside `watchdog` hot paths (like `on_any_event`) causes redundant cache lookups and array iterations. Specifically, evaluating `_is_ignored(dest_path)` before checking if `src_path` is ignored costs CPU time for every valid "moved" file event. Also, placing intent flags (like `setattr(process, '_echo_terminated', True)`) *after* the OS termination call is unsafe: if the process exits right before termination and throws `OSError`, the intent flag is never set. + +Action: +Always lazy-evaluate expensive filters in event-loop hot paths. Always set intent flags *before* executing fallible OS-level state changes to guarantee accurate state tracking in exception handlers. + +## 2026-04-17 — Dead Code in Reload Termination Feedback + +Learning: +When managing subprocesses, if a reload starts a new process, the class attribute `self.current_process` is reassigned immediately. Therefore, in the wait block of the *old* process, checking `self.current_process is process` will evaluate to `False`. This renders any termination reporting logic nested within that block as dead code, leading to silent reloads. + +Action: +Evaluate termination flags (`_echo_terminated`) independently of the "current process" identity check to ensure the correct system feedback is provided regardless of race conditions during reassignment. + +## 2024-04-18 — Process Termination Reporting Dead Code + +Learning: +When evaluating a subprocess's intent-based termination flags after `process.wait()`, guarding the reporting logic with `self.current_process is process` causes it to become dead code. During a command reload, `self.current_process` is reassigned to the new process before the old process's wait block completes, thus failing to report successful execution or failure. + +Action: +Ensure post-termination reporting logic unconditionally logs the outcome when the intention-based check (`_echo_terminated`) is not met, instead of restricting it to the current process reference. + +## 2026-04-18 — Redundant Ignore Evaluation Optimization + +Learning: +In hot paths like `_is_ignored_impl` inside `watchdog` loops, repetitive checks that perform operations already inherently satisfied by earlier checks cost unnecessary CPU cycles. For example, explicitly evaluating whether the first directory layer `parts[0]` matches exact ignores and wildcards is wasteful, as `exact_ignores.isdisjoint(parts)` and iterating over `parts` already validates it earlier in the function. + +Action: +Avoid redundant state re-evaluation on subsets of data in the file watcher's hot path by explicitly reviewing the cascade of earlier boolean checks. + +## 2026-04-20 — Path Normalization Hot-Path Bottleneck + +Learning: +`watchdog` file watchers trigger events with absolute paths. Converting these absolute paths back to relative paths relative to the watch directory `base_path` using `os.path.relpath()` is computationally expensive (approx 10-15x slower than a simple slice). During high-volume file events (like `npm install` or branch changes), this overhead chokes the hot path and introduces measurable lag before commands execute. + +Action: +When implementing `watchdog` ignore filters, normalize absolute event paths to relative paths against the watched `base_path` to ensure wildcard patterns match correctly. For optimal performance, pre-compute the absolute base path with a trailing separator and use a fast string slice (`if path.startswith(self._abs_base_path): path = path[len(self._abs_base_path):]`) before falling back to `os.path.relpath` (wrapped in a `try/except ValueError`). +## 2026-04-21 — Fix path prefix accumulation bug in file ignore logic + +Learning: +An off-by-one bug in array slicing (`parts[1:-1]`) during path matching caused the file watcher to skip exact matching against the full, multi-part path itself. This falsely allowed events on ignored files to trigger commands when the target file path was within a matched ignore directory. + +Action: +Ensure accumulation loops over path components include all elements of the sequence up to the leaf node (i.e., using `parts[1:]`) so that multi-part file patterns are reliably validated against exact ignores. + +## 2026-04-22 — Stream Redirection & Regex Parsing + +Learning: +When providing `stdout` or `stderr` arguments to `subprocess.Popen`, passing `sys.stdout` or `sys.stderr` directly causes a crash (`io.UnsupportedOperation: fileno`) in test environments (e.g., pytest's `capsys`) or GUI wrappers where the streams lack a `.fileno()` method. Additionally, when identifying wildcard patterns for `fnmatch` evaluation, character class brackets `[` must be checked alongside `*` and `?`, otherwise patterns like `[a-z].tmp` are incorrectly treated as exact match strings. + +Action: +Always wrap custom stream targets with a safety check for `.fileno()`, falling back to `None` to safely inherit system-level descriptors. Always include `[` when distinguishing wildcard paths from static paths. +## 2026-04-22 — Ignore Pattern Caching and Redundancy + +Learning: +Inside the `_is_ignored_impl` hot path, `normalized_path in self.exact_ignores` and `self.wildcard_regex.match(normalized_path)` are inherently redundant. `isdisjoint()` evaluates every split part individually. When `normalized_path` itself has no slashes, it is `parts[0]` and caught there. When `normalized_path` contains slashes, the `if len(parts) > 1:` loop explicitly rebuilds the exact same string on the final iteration (e.g. `foo/bar` becomes `prefix` on final loop) and matches it. + +Action: +Removed the top-level checks to save string hashing and regex matching latency on deep recursive paths. + +## 2026-04-23 — Fix _abs_base_path to properly use os.path.join and handle root directory matching + +Learning: +Using string concatenation with `os.sep` for `_abs_base_path` can cause issues when `os.path.abspath` returns a path that already has a separator (e.g. root directory `/`), resulting in `//` and failing the prefix check in `_is_ignored_impl`. + +Action: +Use `os.path.join(os.path.abspath(base_path), '')` to safely handle trailing separators, and update `_is_ignored_impl` to check if `path` exactly matches `self._abs_base_path` (e.g. root directory). This prevents expensive `os.path.relpath` fallbacks for valid ignore pattern matching. +## 2026-04-23 — Ignore Pattern Caching and Redundancy + +Learning: +Inside the `_is_ignored_impl` hot path, `normalized_path in self.exact_ignores` and `self.wildcard_regex.match(normalized_path)` are inherently redundant. `isdisjoint()` evaluates every split part individually. When `normalized_path` itself has no slashes, it is `parts[0]` and caught there. When `normalized_path` contains slashes, the `if len(parts) > 1:` loop explicitly rebuilds the exact same string on the final iteration (e.g. `foo/bar` becomes `prefix` on final loop) and matches it. + +Action: +Removed the top-level checks to save string hashing and regex matching latency on deep recursive paths. + +## 2026-04-24 — CPU Spin Bug in File Watcher Debounce Worker + +Learning: +If the `_debounce_worker` thread receives an event with no valid `path_to_run` (e.g. from an ignored file or empty path string) and `time_to_wait` reaches `<= 0`, it skips the execution block and attempts to `wait` on the shutdown event. Because `time_to_wait <= 0`, `wait(timeout)` returns immediately, causing an infinite while-loop that consumes 100% CPU. Additionally, `on_any_event` allowed falsely truthy null-path events to spawn the debounce thread. + +Action: +Ensure the background `_debounce_worker` thread unconditionally terminates (via `return`) when `time_to_wait <= 0`, executing the command only if the path is valid and no shutdown is requested. Added early returns in `on_any_event` to prevent spawning timers for invalid paths entirely. + +## 2026-04-24 — Test Suite Thread Synchronization Reliability + +Learning: +Tests involving thread execution (like the file watcher's debounce or shutdown threads) must not rely on `time.sleep()` for waiting. Under CI/coverage load, these static sleeps are prone to flakiness due to scheduling overhead, causing assertions against thread termination state to falsely fail. + + +## 2026-04-24 — Test Suite Dynamic Polling Fix + +Learning: +Using `.join()` unconditionally to replace `time.sleep()` in test cases is a flawed approach because `join()` halts the test thread until the target thread completely finishes its execution. For file watcher tests involving processes that are expected to be running or terminating, the assertions need to test an intermediate state. Unconditional joins bypass this intermediate state and test the end state, missing the intent. + +Action: +Instead of `time.sleep()`, tests should use dynamic polling mechanisms (`while handler.current_process is None` coupled with short `time.sleep(0.05)` cycles and a maximum timeout) to efficiently wait only until the desired intermediate condition is met. This ensures the tests run significantly faster while preventing flakiness. + +## 2026-04-24 — Rich Markup Error Bug + +Learning: +When passing raw user strings containing square brackets (like file paths, directories, or bash commands) into `rich.console.print` format strings, `rich` attempts to parse them as style markup tags (e.g., `[red]`). If the string inside the brackets is not a valid tag, or if there's a typo/unclosed tag, the library throws a `MarkupError` exception which will crash the thread executing the print statement. + +Action: +Always use `rich.markup.escape(str(variable))` before injecting unvalidated user-provided strings into `rich` print statements to guarantee safe output. + +## 2026-04-28 — Pre-computing `_base_prefix` for Fast-Path Slicing + +Learning: +Inside the `_is_ignored_impl` hot path in `watchdog`, calling `os.path.relpath` for relative event paths when they could be sliced using `len(self._base_prefix)` introduced measurable latency in high-volume events. Additionally, generically calling `.removeprefix('./')` on paths could cause unexpected resolution regressions. + +Action: +Pre-compute `_base_prefix` during initialization (`os.path.join(self.base_path, '')`) and use it in `startswith()` alongside `_abs_base_path` for fast string slicing. Also removed the blind `.removeprefix('./')` behavior to improve robustness. + +## 2026-04-29 — Reliability Fix for SIGTERM + +Learning: +Command-line file watchers and daemon tools usually listen for KeyboardInterrupt (SIGINT) to clean up subprocesses gracefully. However, they often ignore SIGTERM, which is the standard termination signal sent by containers (Docker/K8s) and process managers. Ignoring SIGTERM causes the main watcher to die instantly, leaking running child processes in the background indefinitely and causing resource exhaustion. + +Action: +Always register a SIGTERM handler on POSIX systems (`if platform.system() != "Windows"`) that performs the same graceful shutdown and subprocess termination steps as the KeyboardInterrupt handler. + +## 2026-04-29 — Ignore Filter Relpath & Compound Loop Overhead + +Learning: +Inside the `_is_ignored_impl` hot path, `os.path.relpath` is computationally expensive because it inherently resolves absolute paths. While optimizations existed for exact prefix matching, simple relative paths (e.g., `src/file.py`) against a `.` base path would fall through and trigger a `relpath` call, slowing down high-volume events. Additionally, reconstructing cumulative directory prefixes (`foo`, `foo/bar`) to test against exact/wildcard ignores consumes significant CPU cycles and is entirely unnecessary if the user specified no compound ignore patterns (i.e., no slashes in any pattern). + +Action: +In `watchdog` event path normalization, bypass the computationally expensive `os.path.relpath` for the common case where `base_path` is `.` and the path is already relative by adding a fast-path condition: `elif self.base_path == "." and not os.path.isabs(path) and not path.startswith(".."): pass`. +To optimize ignore pattern matching in hot loops, pre-compute a flag during initialization (e.g., `self._has_compound_ignores = any('/' in p for p in self.ignore_patterns)`) and use it to short-circuit the evaluation of compound directory paths if no slash-based ignore patterns exist. + +## 2026-05-01 — Wildcard Regex Split Optimization + +Learning: +Inside the file watcher's `_is_ignored_impl` hot path, applying a combined wildcard regex that includes both simple patterns (e.g. `*.tmp`) and compound patterns (e.g. `src/*.tmp`) to individual path segments (`parts`) and cumulative directory prefixes (`prefix`) is redundant and computationally wasteful. A simple wildcard pattern incorrectly evaluated against a cumulative prefix path loop wastes time, and a compound wildcard will never match a simple directory segment. + +Action: +Split wildcard patterns into `simple_wildcards` (no slashes) and `compound_wildcards` (contains slashes), and compile them into separate regular expressions (`simple_wildcard_regex` and `compound_wildcard_regex`). Only apply the simple regex when iterating over individual parts, and apply the compound regex when accumulating the directory prefix. This optimization prevents unnecessary regex checks in the hot path. diff --git a/.jules/warden.md b/.jules/warden.md index 9fff46b..58c7152 100644 --- a/.jules/warden.md +++ b/.jules/warden.md @@ -176,3 +176,11 @@ Observed the preceding agent optimized the ignore file watcher hot paths by expl Alignment / Deferred: Version bumped to `0.1.23` as a patch release. Updated CHANGELOG.md. + +## 2026-05-02 — Assessment & Lifecycle + +Observation / Pruned: +Observed the preceding agent optimized wildcard ignore patterns by separating them into simple and compound matchers, avoiding redundant regex evaluations in the hot path. Tests passed successfully and static analysis tools confirmed no dead code or lint issues. + +Alignment / Deferred: +Version bumped to `0.1.24` as a patch release. Updated CHANGELOG.md. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbc4e5..788446c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog +## [0.1.24] - 2026-05-02 + +### Changed +* **[Performance]:** Split wildcard ignore patterns into simple and compound regexes to prevent redundant evaluations during path checking, improving file event performance. + ## [0.1.23] - 2026-04-30 diff --git a/pyproject.toml b/pyproject.toml index e3f415f..39f7f5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "echo-watcher" -version = "0.1.23" +version = "0.1.24" description = "📡 Lightweight file watcher. Trigger commands on changes. <5MB RAM, single binary." authors = [ { name = "shenald-dev", email = "bot@shenald.dev" } @@ -16,4 +16,4 @@ echo-watch = "echo.watcher:main" [build-system] requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" diff --git a/src/echo/watcher.py b/src/echo/watcher.py index 1d178c2..dda5d3b 100644 --- a/src/echo/watcher.py +++ b/src/echo/watcher.py @@ -32,11 +32,18 @@ def __init__(self, command: str, base_path: str = ".", ignore_patterns: list[str # Pre-compute exact vs wildcard patterns for faster matching self.exact_ignores = {p for p in self.ignore_patterns if not any(c in p for c in ('*', '?', '['))} wildcard_ignores = [p for p in self.ignore_patterns if any(c in p for c in ('*', '?', '['))] - self.wildcard_regex = None + + simple_wildcards = [p for p in wildcard_ignores if '/' not in p] + compound_wildcards = [p for p in wildcard_ignores if '/' in p] + + self.simple_wildcard_regex = None + self.compound_wildcard_regex = None self._has_compound_ignores = any('/' in p for p in self.ignore_patterns) - if wildcard_ignores: - regex_str = "|".join(f"(?:{fnmatch.translate(p)})" for p in wildcard_ignores) - self.wildcard_regex = re.compile(regex_str) + + if simple_wildcards: + self.simple_wildcard_regex = re.compile("|".join(f"(?:{fnmatch.translate(p)})" for p in simple_wildcards)) + if compound_wildcards: + self.compound_wildcard_regex = re.compile("|".join(f"(?:{fnmatch.translate(p)})" for p in compound_wildcards)) self.current_process = None self.process_lock = threading.Lock() @@ -188,9 +195,9 @@ def _is_ignored_impl(self, path: str) -> bool: if not self.exact_ignores.isdisjoint(parts): return True - if self.wildcard_regex: + if self.simple_wildcard_regex: for part in parts: - if self.wildcard_regex.match(part): + if self.simple_wildcard_regex.match(part): return True # Check for exact and wildcard ignore patterns matching cumulative prefix directories @@ -203,7 +210,7 @@ def _is_ignored_impl(self, path: str) -> bool: prefix = f"{prefix}/{part}" if prefix in self.exact_ignores: return True - if self.wildcard_regex and self.wildcard_regex.match(prefix): + if self.compound_wildcard_regex and self.compound_wildcard_regex.match(prefix): return True return False diff --git a/tests/test_ignore.py b/tests/test_ignore.py index e8816de..92c068e 100644 --- a/tests/test_ignore.py +++ b/tests/test_ignore.py @@ -126,7 +126,7 @@ def test_character_class_wildcard_match(): handler = CommandRunnerHandler("echo 1", ignore_patterns=["[a-z].tmp"]) # Must correctly categorize as wildcard and compile regex - assert handler.wildcard_regex is not None + assert handler.simple_wildcard_regex is not None assert "[a-z].tmp" not in handler.exact_ignores assert handler._is_ignored("a.tmp") is True From 77d4dd9bcfcb85c7b42896f0cc29708f1a45126d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 20:51:16 +0000 Subject: [PATCH 13/13] Merge main and resolve conflicts Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com>