story-kit: done 225_story_surface_merge_conflicts_and_failures_in_the_web_ui
This commit is contained in:
@@ -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"
|
name = "coder-1"
|
||||||
stage = "coder"
|
stage = "coder"
|
||||||
role = "Full-stack engineer. Implements features across all components."
|
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."
|
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
|
/// Resolve a path argument supplied on the CLI against the given working
|
||||||
/// directory. Relative paths (including `.`) are joined with `cwd` and
|
/// directory. Relative paths (including `.`) are joined with `cwd` and
|
||||||
/// then canonicalized when possible. Absolute paths are returned
|
/// 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))?;
|
.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("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(&specs_root.join("00_CONTEXT.md"), STORY_KIT_CONTEXT)?;
|
||||||
write_file_if_missing(&tech_root.join("STACK.md"), STORY_KIT_STACK)?;
|
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_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
|
// Path doesn't exist yet — canonicalize fails, fallback is cwd/newproject
|
||||||
assert_eq!(result, cwd.join("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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`.
|
The script must start with `#!/usr/bin/env bash` and `set -euo pipefail`.
|
||||||
|
|
||||||
## Step 4: Project Configuration
|
## Step 4: Project Configuration
|
||||||
Use `write_file` to write `.story_kit/project.toml` with `[[component]]` entries that match the chosen stack. Each component needs:
|
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.
|
||||||
- `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)
|
|
||||||
|
|
||||||
Also include at least one `[[agent]]` entry for a coder agent:
|
First, inspect the project structure to identify the tech stack:
|
||||||
```toml
|
- Use `list_directory(".")` to see top-level files and directories
|
||||||
[[agent]]
|
- Look for tech stack markers: `Cargo.toml` (Rust/Cargo), `package.json` (Node/frontend), `pyproject.toml` or `requirements.txt` (Python), `go.mod` (Go), `Gemfile` (Ruby)
|
||||||
name = "coder-1"
|
- Check subdirectories like `frontend/`, `backend/`, `app/`, `web/` for nested stacks
|
||||||
stage = "coder"
|
- If you find a `package.json`, check whether `pnpm-lock.yaml`, `yarn.lock`, or `package-lock.json` exists to determine the package manager
|
||||||
role = "Implements features across all components."
|
|
||||||
model = "sonnet"
|
Then use `read_file(".story_kit/project.toml")` to see the current content, keeping the `[[agent]]` sections intact.
|
||||||
max_turns = 50
|
|
||||||
max_budget_usd = 5.00
|
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
|
## Step 5: Commit & Finish
|
||||||
After writing all files:
|
After writing all files:
|
||||||
|
|||||||
Reference in New Issue
Block a user