diff --git a/.huskies/.gitignore b/.huskies/.gitignore index 62fd2d7b..1282a42a 100644 --- a/.huskies/.gitignore +++ b/.huskies/.gitignore @@ -35,3 +35,6 @@ double_timmy_log.md pipeline.db pipeline.db.bak* session_store.json + +# Full untruncated merge attempt reports (one file per attempt, pruned automatically) +merge_reports/ diff --git a/server/src/io/fs/project.rs b/server/src/io/fs/project.rs index 91ef1015..af11fac1 100644 --- a/server/src/io/fs/project.rs +++ b/server/src/io/fs/project.rs @@ -5,7 +5,7 @@ use serde_json::json; use std::fs; use std::path::PathBuf; -use super::scaffold::scaffold_story_kit; +use super::scaffold::{ensure_gitignore_entries, scaffold_story_kit}; const KEY_LAST_PROJECT: &str = "last_project_path"; const KEY_KNOWN_PROJECTS: &str = "known_projects"; @@ -36,6 +36,10 @@ pub(crate) async fn ensure_project_root_with_story_kit( } if !path.join(".huskies").is_dir() { scaffold_story_kit(&path, port)?; + } else { + // Already-adopted project: pick up any Story Kit gitignore entries + // added since this project was first scaffolded. + ensure_gitignore_entries(&path)?; } // Always update .mcp.json with the current port so the bot connects to // the right endpoint even when HUSKIES_PORT changes between restarts. @@ -449,6 +453,34 @@ mod tests { assert!(project_dir.join(".huskies").is_dir()); } + /// Regression test for story 1219: projects scaffolded before + /// `merge_reports/` and `session_store.json` were added to the ignore + /// list must pick up those entries the next time the project is opened, + /// without needing to be re-scaffolded from scratch. + #[tokio::test] + async fn open_project_retrofits_gitignore_entries_for_existing_project() { + let dir = tempdir().unwrap(); + let project_dir = dir.path().join("myproject"); + let sk_dir = project_dir.join(".huskies"); + fs::create_dir_all(&sk_dir).unwrap(); + fs::write(sk_dir.join(".gitignore"), "worktrees/\n").unwrap(); + let store = make_store(&dir); + let state = SessionState::default(); + + open_project( + project_dir.to_string_lossy().to_string(), + &state, + &store, + 3001, + ) + .await + .unwrap(); + + let content = fs::read_to_string(sk_dir.join(".gitignore")).unwrap(); + assert!(content.contains("merge_reports/")); + assert!(content.contains("session_store.json")); + } + #[tokio::test] async fn open_project_does_not_overwrite_existing_story_kit() { let dir = tempdir().unwrap(); diff --git a/server/src/io/fs/scaffold/helpers.rs b/server/src/io/fs/scaffold/helpers.rs index 32a01a0b..5b88b526 100644 --- a/server/src/io/fs/scaffold/helpers.rs +++ b/server/src/io/fs/scaffold/helpers.rs @@ -64,6 +64,8 @@ pub(super) fn write_story_kit_gitignore(root: &Path) -> Result<(), String> { "store.json", "pipeline.db", "*.db", + "merge_reports/", + "session_store.json", ]; let gitignore_path = root.join(".huskies").join(".gitignore"); diff --git a/server/src/io/fs/scaffold/mod.rs b/server/src/io/fs/scaffold/mod.rs index b2acc1c8..ed7c19c9 100644 --- a/server/src/io/fs/scaffold/mod.rs +++ b/server/src/io/fs/scaffold/mod.rs @@ -20,6 +20,16 @@ use templates::{ STORY_KIT_STACK, }; +/// Retrofit the Story Kit `.gitignore` entries onto a project that was +/// scaffolded before those entries existed (e.g. `merge_reports/` or +/// `session_store.json` added in a later release). Idempotent — only +/// appends lines that are missing, so it is safe to call on every project +/// open regardless of how old the project's `.huskies/` directory is. +pub(crate) fn ensure_gitignore_entries(root: &Path) -> Result<(), String> { + write_story_kit_gitignore(root)?; + append_root_gitignore_entries(root) +} + pub(crate) fn scaffold_story_kit(root: &Path, port: u16) -> Result<(), String> { let story_kit_root = root.join(".huskies"); let specs_root = story_kit_root.join("specs"); diff --git a/server/src/io/fs/scaffold/tests.rs b/server/src/io/fs/scaffold/tests.rs index b0aa63a6..2e44990d 100644 --- a/server/src/io/fs/scaffold/tests.rs +++ b/server/src/io/fs/scaffold/tests.rs @@ -334,6 +334,31 @@ fn scaffold_creates_story_kit_gitignore_with_relative_entries() { // Database files must be ignored so novice users don't accidentally commit them assert!(sk_content.contains("pipeline.db")); assert!(sk_content.contains("*.db")); + // Runtime artifacts written under .huskies/ must be ignored too (story 1219) + assert!(sk_content.contains("merge_reports/")); + assert!(sk_content.contains("session_store.json")); +} + +#[test] +fn ensure_gitignore_entries_retrofits_already_adopted_project() { + // Simulate a project scaffolded before merge_reports/ and session_store.json + // were added to the ignore list: .huskies/.gitignore exists but lacks them. + let dir = tempdir().unwrap(); + fs::create_dir_all(dir.path().join(".huskies")).unwrap(); + fs::write( + dir.path().join(".huskies/.gitignore"), + "worktrees/\ncoverage/\n", + ) + .unwrap(); + + ensure_gitignore_entries(dir.path()).unwrap(); + + let sk_content = fs::read_to_string(dir.path().join(".huskies/.gitignore")).unwrap(); + assert!(sk_content.contains("merge_reports/")); + assert!(sk_content.contains("session_store.json")); + // Pre-existing entries must survive untouched + assert!(sk_content.contains("worktrees/")); + assert!(sk_content.contains("coverage/")); } #[test]