From 7574e3b4bce6d1a90795987fb32bc6ee9ec36f17 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 27 Feb 2026 16:41:20 +0000 Subject: [PATCH] story-kit: done 225_story_surface_merge_conflicts_and_failures_in_the_web_ui --- ...ge_conflicts_and_failures_in_the_web_ui.md | 0 server/src/io/fs.rs | 285 +++++++++++++++++- server/src/llm/prompts.rs | 31 +- 3 files changed, 299 insertions(+), 17 deletions(-) rename .story_kit/work/{4_merge => 5_done}/225_story_surface_merge_conflicts_and_failures_in_the_web_ui.md (100%) diff --git a/.story_kit/work/4_merge/225_story_surface_merge_conflicts_and_failures_in_the_web_ui.md b/.story_kit/work/5_done/225_story_surface_merge_conflicts_and_failures_in_the_web_ui.md similarity index 100% rename from .story_kit/work/4_merge/225_story_surface_merge_conflicts_and_failures_in_the_web_ui.md rename to .story_kit/work/5_done/225_story_surface_merge_conflicts_and_failures_in_the_web_ui.md diff --git a/server/src/io/fs.rs b/server/src/io/fs.rs index 7429373..40f4435 100644 --- a/server/src/io/fs.rs +++ b/server/src/io/fs.rs @@ -100,7 +100,7 @@ const STORY_KIT_CLAUDE_SETTINGS: &str = r#"{ } "#; -const DEFAULT_PROJECT_TOML: &str = r#"[[agent]] +const DEFAULT_PROJECT_AGENTS_TOML: &str = r#"[[agent]] name = "coder-1" stage = "coder" role = "Full-stack engineer. Implements features across all components." @@ -131,6 +131,96 @@ prompt = "You are the mergemaster agent for story {{story_id}}. Call merge_agent system_prompt = "You are the mergemaster agent. Trigger merge_agent_work via MCP and report results. Never manually move story files. Call report_merge_failure when merges fail." "#; +/// Detect the tech stack from the project root and return TOML `[[component]]` entries. +/// +/// Inspects well-known marker files at the project root to identify which +/// tech stacks are present, then emits one `[[component]]` entry per detected +/// stack with sensible default `setup` commands. If no markers are found, a +/// single fallback `app` component with an empty `setup` list is returned so +/// that the pipeline never breaks on an unknown stack. +pub fn detect_components_toml(root: &Path) -> String { + let mut sections = Vec::new(); + + if root.join("Cargo.toml").exists() { + sections.push( + "[[component]]\nname = \"server\"\npath = \".\"\nsetup = [\"cargo check\"]\n" + .to_string(), + ); + } + + if root.join("package.json").exists() { + let setup_cmd = if root.join("pnpm-lock.yaml").exists() { + "pnpm install" + } else { + "npm install" + }; + sections.push(format!( + "[[component]]\nname = \"frontend\"\npath = \".\"\nsetup = [\"{setup_cmd}\"]\n" + )); + } + + if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() { + sections.push( + "[[component]]\nname = \"python\"\npath = \".\"\nsetup = [\"pip install -r requirements.txt\"]\n" + .to_string(), + ); + } + + if root.join("go.mod").exists() { + sections.push( + "[[component]]\nname = \"go\"\npath = \".\"\nsetup = [\"go build ./...\"]\n" + .to_string(), + ); + } + + if root.join("Gemfile").exists() { + sections.push( + "[[component]]\nname = \"ruby\"\npath = \".\"\nsetup = [\"bundle install\"]\n" + .to_string(), + ); + } + + if sections.is_empty() { + // No tech stack markers detected — emit two example components so that + // the scaffold is immediately usable and agents can see the expected + // format. The ONBOARDING_PROMPT instructs the chat agent to inspect + // the project and replace these placeholders with real definitions. + sections.push( + "# EXAMPLE: Replace with your actual backend component.\n\ + # Common patterns: \"cargo check\" (Rust), \"go build ./...\" (Go),\n\ + # \"python -m pytest\" (Python), \"mvn verify\" (Java)\n\ + [[component]]\n\ + name = \"backend\"\n\ + path = \".\"\n\ + setup = [\"cargo check\"]\n\ + teardown = []\n" + .to_string(), + ); + sections.push( + "# EXAMPLE: Replace with your actual frontend component.\n\ + # Common patterns: \"pnpm install\" (pnpm), \"npm install\" (npm),\n\ + # \"yarn\" (Yarn), \"bun install\" (Bun)\n\ + [[component]]\n\ + name = \"frontend\"\n\ + path = \".\"\n\ + setup = [\"pnpm install\"]\n\ + teardown = []\n" + .to_string(), + ); + } + + sections.join("\n") +} + +/// Generate a complete `project.toml` for a new project at `root`. +/// +/// Detects the tech stack via [`detect_components_toml`] and prepends the +/// resulting `[[component]]` entries before the default `[[agent]]` sections. +fn generate_project_toml(root: &Path) -> String { + let components = detect_components_toml(root); + format!("{components}\n{DEFAULT_PROJECT_AGENTS_TOML}") +} + /// Resolve a path argument supplied on the CLI against the given working /// directory. Relative paths (including `.`) are joined with `cwd` and /// then canonicalized when possible. Absolute paths are returned @@ -297,7 +387,8 @@ fn scaffold_story_kit(root: &Path) -> Result<(), String> { .map_err(|e| format!("Failed to create script/ directory: {}", e))?; write_file_if_missing(&story_kit_root.join("README.md"), STORY_KIT_README)?; - write_file_if_missing(&story_kit_root.join("project.toml"), DEFAULT_PROJECT_TOML)?; + let project_toml_content = generate_project_toml(root); + write_file_if_missing(&story_kit_root.join("project.toml"), &project_toml_content)?; 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)?; @@ -1254,4 +1345,194 @@ mod tests { // Path doesn't exist yet — canonicalize fails, fallback is cwd/newproject assert_eq!(result, cwd.join("newproject")); } + + // --- detect_components_toml --- + + #[test] + fn detect_no_markers_returns_fallback_components() { + let dir = tempdir().unwrap(); + let toml = detect_components_toml(dir.path()); + // At least one [[component]] entry should always be present + assert!( + toml.contains("[[component]]"), + "should always emit at least one component" + ); + // The fallback should include example backend and frontend entries + assert!( + toml.contains("name = \"backend\"") || toml.contains("name = \"frontend\""), + "fallback should include example component entries" + ); + } + + #[test] + fn detect_cargo_toml_generates_rust_component() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"test\"\n").unwrap(); + + let toml = detect_components_toml(dir.path()); + assert!(toml.contains("name = \"server\"")); + assert!(toml.contains("setup = [\"cargo check\"]")); + } + + #[test] + fn detect_package_json_with_pnpm_lock_generates_pnpm_component() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("package.json"), "{}").unwrap(); + fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap(); + + let toml = detect_components_toml(dir.path()); + assert!(toml.contains("name = \"frontend\"")); + assert!(toml.contains("setup = [\"pnpm install\"]")); + } + + #[test] + fn detect_package_json_without_pnpm_lock_generates_npm_component() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("package.json"), "{}").unwrap(); + + let toml = detect_components_toml(dir.path()); + assert!(toml.contains("name = \"frontend\"")); + assert!(toml.contains("setup = [\"npm install\"]")); + } + + #[test] + fn detect_pyproject_toml_generates_python_component() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("pyproject.toml"), "[project]\nname = \"test\"\n").unwrap(); + + let toml = detect_components_toml(dir.path()); + assert!(toml.contains("name = \"python\"")); + assert!(toml.contains("pip install")); + } + + #[test] + fn detect_requirements_txt_generates_python_component() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("requirements.txt"), "flask\n").unwrap(); + + let toml = detect_components_toml(dir.path()); + assert!(toml.contains("name = \"python\"")); + assert!(toml.contains("pip install")); + } + + #[test] + fn detect_go_mod_generates_go_component() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("go.mod"), "module example.com/app\n").unwrap(); + + let toml = detect_components_toml(dir.path()); + assert!(toml.contains("name = \"go\"")); + assert!(toml.contains("setup = [\"go build ./...\"]")); + } + + #[test] + fn detect_gemfile_generates_ruby_component() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("Gemfile"), "source \"https://rubygems.org\"\n").unwrap(); + + let toml = detect_components_toml(dir.path()); + assert!(toml.contains("name = \"ruby\"")); + assert!(toml.contains("setup = [\"bundle install\"]")); + } + + #[test] + fn detect_multiple_markers_generates_multiple_components() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"server\"\n").unwrap(); + fs::write(dir.path().join("package.json"), "{}").unwrap(); + + let toml = detect_components_toml(dir.path()); + assert!(toml.contains("name = \"server\"")); + assert!(toml.contains("name = \"frontend\"")); + // Both component entries should be present + let component_count = toml.matches("[[component]]").count(); + assert_eq!(component_count, 2); + } + + #[test] + fn detect_no_fallback_when_markers_found() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap(); + + let toml = detect_components_toml(dir.path()); + // The fallback "app" component should NOT appear when a real stack is detected + assert!(!toml.contains("name = \"app\"")); + } + + // --- generate_project_toml --- + + #[test] + fn generate_project_toml_includes_both_components_and_agents() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap(); + + let toml = generate_project_toml(dir.path()); + // Component section + assert!(toml.contains("[[component]]")); + assert!(toml.contains("name = \"server\"")); + // Agent sections + assert!(toml.contains("[[agent]]")); + assert!(toml.contains("stage = \"coder\"")); + assert!(toml.contains("stage = \"qa\"")); + assert!(toml.contains("stage = \"mergemaster\"")); + } + + #[test] + fn scaffold_project_toml_contains_detected_components() { + let dir = tempdir().unwrap(); + // Place a Cargo.toml in the project root before scaffolding + fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"myapp\"\n").unwrap(); + + scaffold_story_kit(dir.path()).unwrap(); + + let content = + fs::read_to_string(dir.path().join(".story_kit/project.toml")).unwrap(); + assert!( + content.contains("[[component]]"), + "project.toml should contain a component entry" + ); + assert!( + content.contains("name = \"server\""), + "Rust project should have a 'server' component" + ); + assert!( + content.contains("cargo check"), + "Rust component should have cargo check setup" + ); + } + + #[test] + fn scaffold_project_toml_fallback_when_no_stack_detected() { + let dir = tempdir().unwrap(); + scaffold_story_kit(dir.path()).unwrap(); + + let content = + fs::read_to_string(dir.path().join(".story_kit/project.toml")).unwrap(); + assert!( + content.contains("[[component]]"), + "project.toml should always have at least one component" + ); + // Fallback emits example components so the scaffold is immediately usable + assert!( + content.contains("name = \"backend\"") || content.contains("name = \"frontend\""), + "fallback should include example component entries" + ); + } + + #[test] + fn scaffold_does_not_overwrite_existing_project_toml_with_components() { + let dir = tempdir().unwrap(); + let sk_dir = dir.path().join(".story_kit"); + fs::create_dir_all(&sk_dir).unwrap(); + let existing = "[[component]]\nname = \"custom\"\npath = \".\"\nsetup = [\"make build\"]\n"; + fs::write(sk_dir.join("project.toml"), existing).unwrap(); + + scaffold_story_kit(dir.path()).unwrap(); + + let content = fs::read_to_string(sk_dir.join("project.toml")).unwrap(); + assert_eq!( + content, existing, + "scaffold should not overwrite existing project.toml" + ); + } } diff --git a/server/src/llm/prompts.rs b/server/src/llm/prompts.rs index 51cebcd..d279854 100644 --- a/server/src/llm/prompts.rs +++ b/server/src/llm/prompts.rs @@ -131,22 +131,23 @@ Based on the tech stack answers, use `write_file` to write `script/test` — a b The script must start with `#!/usr/bin/env bash` and `set -euo pipefail`. ## Step 4: Project Configuration -Use `write_file` to write `.story_kit/project.toml` with `[[component]]` entries that match the chosen stack. Each component needs: -- `name` — component identifier (e.g. "backend", "frontend", "app") -- `path` — relative path from project root (use "." for root) -- `setup` — list of setup commands (e.g. ["pnpm install"], ["cargo check"]) -- `teardown` — list of cleanup commands (usually empty) +The scaffold has written `.story_kit/project.toml` with example `[[component]]` sections. You must replace these examples with real definitions that match the project's actual tech stack. -Also include at least one `[[agent]]` entry for a coder agent: -```toml -[[agent]] -name = "coder-1" -stage = "coder" -role = "Implements features across all components." -model = "sonnet" -max_turns = 50 -max_budget_usd = 5.00 -``` +First, inspect the project structure to identify the tech stack: +- Use `list_directory(".")` to see top-level files and directories +- Look for tech stack markers: `Cargo.toml` (Rust/Cargo), `package.json` (Node/frontend), `pyproject.toml` or `requirements.txt` (Python), `go.mod` (Go), `Gemfile` (Ruby) +- Check subdirectories like `frontend/`, `backend/`, `app/`, `web/` for nested stacks +- If you find a `package.json`, check whether `pnpm-lock.yaml`, `yarn.lock`, or `package-lock.json` exists to determine the package manager + +Then use `read_file(".story_kit/project.toml")` to see the current content, keeping the `[[agent]]` sections intact. + +Finally, use `write_file` to rewrite `.story_kit/project.toml` with real `[[component]]` entries. Each component needs: +- `name` — component identifier (e.g. "backend", "frontend", "app") +- `path` — relative path from project root (use "." for root, "frontend" for a frontend subdirectory) +- `setup` — list of setup commands that install dependencies and verify the build (e.g. ["pnpm install"], ["cargo check"]) +- `teardown` — list of cleanup commands (usually []) + +Preserve all `[[agent]]` entries from the existing file. Only replace the `[[component]]` sections. ## Step 5: Commit & Finish After writing all files: