From 733b92337ed19b0fc1743b92f13d6303eee021b4 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 26 Feb 2026 18:29:32 +0000 Subject: [PATCH] story-kit: merge 217_story_scaffold_generates_claude_md --- server/src/io/fs.rs | 51 ++++++++++++++++++++++++++++++++++++- server/src/io/onboarding.rs | 37 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/server/src/io/fs.rs b/server/src/io/fs.rs index 1c754ea..8b30a42 100644 --- a/server/src/io/fs.rs +++ b/server/src/io/fs.rs @@ -52,6 +52,14 @@ TODO: List approved libraries and their purpose.\n"; const STORY_KIT_SCRIPT_TEST: &str = "#!/usr/bin/env bash\nset -euo pipefail\n\n# Add your project's test commands here.\n# Story Kit agents invoke this script as the canonical test runner.\n# Exit 0 on success, non-zero on failure.\necho \"No tests configured\"\n"; +const STORY_KIT_CLAUDE_MD: &str = "\n\ +Never chain shell commands with `&&`, `||`, or `;` in a single Bash call. \ +The permission system validates the entire command string, and chained commands \ +won't match allow rules like `Bash(git *)`. Use separate Bash calls instead — \ +parallel calls work fine.\n\ +\n\ +Read .story_kit/README.md to see our dev process.\n"; + const DEFAULT_PROJECT_TOML: &str = r#"[[agent]] name = "coder-1" stage = "coder" @@ -253,6 +261,7 @@ fn scaffold_story_kit(root: &Path) -> Result<(), String> { write_file_if_missing(&specs_root.join("00_CONTEXT.md"), STORY_KIT_CONTEXT)?; write_file_if_missing(&tech_root.join("STACK.md"), STORY_KIT_STACK)?; write_script_if_missing(&script_root.join("test"), STORY_KIT_SCRIPT_TEST)?; + write_file_if_missing(&root.join("CLAUDE.md"), STORY_KIT_CLAUDE_MD)?; append_gitignore_entries(root)?; @@ -268,7 +277,7 @@ fn scaffold_story_kit(root: &Path) -> Result<(), String> { } let add_output = std::process::Command::new("git") - .args(["add", ".story_kit", "script", ".gitignore"]) + .args(["add", ".story_kit", "script", ".gitignore", "CLAUDE.md"]) .current_dir(root) .output() .map_err(|e| format!("Failed to run git add: {}", e))?; @@ -1074,6 +1083,46 @@ mod tests { assert!(content.contains("store.json")); } + // --- CLAUDE.md scaffold --- + + #[test] + fn scaffold_creates_claude_md_at_project_root() { + let dir = tempdir().unwrap(); + scaffold_story_kit(dir.path()).unwrap(); + + let claude_md = dir.path().join("CLAUDE.md"); + assert!(claude_md.exists(), "CLAUDE.md should be created at project root"); + + let content = fs::read_to_string(&claude_md).unwrap(); + assert!( + content.contains(""), + "CLAUDE.md should contain the scaffold sentinel" + ); + assert!( + content.contains("Read .story_kit/README.md"), + "CLAUDE.md should include directive to read .story_kit/README.md" + ); + assert!( + content.contains("Never chain shell commands"), + "CLAUDE.md should include command chaining rule" + ); + } + + #[test] + fn scaffold_does_not_overwrite_existing_claude_md() { + let dir = tempdir().unwrap(); + let claude_md = dir.path().join("CLAUDE.md"); + fs::write(&claude_md, "custom CLAUDE.md content").unwrap(); + + scaffold_story_kit(dir.path()).unwrap(); + + assert_eq!( + fs::read_to_string(&claude_md).unwrap(), + "custom CLAUDE.md content", + "scaffold should not overwrite an existing CLAUDE.md" + ); + } + // --- open_project scaffolding --- #[tokio::test] diff --git a/server/src/io/onboarding.rs b/server/src/io/onboarding.rs index f2e0622..871933a 100644 --- a/server/src/io/onboarding.rs +++ b/server/src/io/onboarding.rs @@ -254,6 +254,43 @@ mod tests { assert!(!status.needs_project_toml); } + // ── CLAUDE.md is not an onboarding step ────────────────────── + + #[test] + fn onboarding_status_does_not_check_claude_md() { + let dir = TempDir::new().unwrap(); + let root = setup_project(&dir); + + // Write real content for the required onboarding files + fs::write( + root.join(".story_kit/specs/00_CONTEXT.md"), + "# My Project\n\nReal project context.", + ) + .unwrap(); + fs::write( + root.join(".story_kit/specs/tech/STACK.md"), + "# My Stack\n\nReal stack content.", + ) + .unwrap(); + + // CLAUDE.md is absent — should NOT affect onboarding result + assert!(!root.join("CLAUDE.md").exists()); + + let status = check_onboarding_status(&root); + assert!( + !status.needs_context, + "needs_context should be false with real content" + ); + assert!( + !status.needs_stack, + "needs_stack should be false with real content" + ); + assert!( + !status.needs_onboarding(), + "needs_onboarding() should be false regardless of CLAUDE.md presence" + ); + } + // ── partial onboarding ──────────────────────────────────────── #[test]