diff --git a/server/src/io/watcher.rs b/server/src/io/watcher.rs index ced9b9e..54bf5cf 100644 --- a/server/src/io/watcher.rs +++ b/server/src/io/watcher.rs @@ -51,7 +51,19 @@ fn stage_metadata(stage: &str, item_id: &str) -> Option<(&'static str, String)> /// Return the pipeline stage name for a path if it is a `.md` file living /// directly inside one of the known work subdirectories, otherwise `None`. +/// +/// Explicitly returns `None` for any path under `.story_kit/worktrees/` so +/// that code changes made by agents in their isolated worktrees are never +/// auto-committed to master by the watcher. fn stage_for_path(path: &Path) -> Option { + // Reject any path that passes through the worktrees directory. + if path + .components() + .any(|c| c.as_os_str() == "worktrees") + { + return None; + } + if path.extension().is_none_or(|e| e != "md") { return None; } @@ -267,6 +279,31 @@ mod tests { ); } + #[test] + fn stage_for_path_ignores_worktree_paths() { + let worktrees = PathBuf::from("/proj/.story_kit/worktrees"); + + // Code changes inside a worktree must be ignored. + assert_eq!( + stage_for_path(&worktrees.join("42_story_foo/server/src/main.rs")), + None, + ); + + // Even if a worktree happens to contain a path component that looks + // like a pipeline stage, it must still be ignored. + assert_eq!( + stage_for_path(&worktrees.join("42_story_foo/.story_kit/work/2_current/42_story_foo.md")), + None, + ); + + // A path that only contains the word "worktrees" as part of a longer + // segment (not an exact component) must NOT be filtered out. + assert_eq!( + stage_for_path(&PathBuf::from("/proj/.story_kit/work/2_current/not_worktrees_story.md")), + Some("2_current".to_string()), + ); + } + #[test] fn stage_metadata_returns_correct_actions() { let (action, msg) = stage_metadata("2_current", "42_story_foo").unwrap();