story-kit: merge 78_story_create_spike_mcp_tool
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::config::ProjectConfig;
|
||||
use crate::http::context::AppContext;
|
||||
use crate::http::settings::get_editor_command_from_store;
|
||||
use crate::http::workflow::{
|
||||
check_criterion_in_file, create_bug_file, create_story_file, list_bug_files,
|
||||
check_criterion_in_file, create_bug_file, create_spike_file, create_story_file, list_bug_files,
|
||||
load_upcoming_stories, validate_story_dirs,
|
||||
};
|
||||
use crate::worktree;
|
||||
@@ -614,6 +614,24 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
"required": ["story_id", "criterion_index"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create_spike",
|
||||
"description": "Create a spike file in .story_kit/work/1_upcoming/ with a deterministic filename and YAML front matter. Returns the spike_id.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable spike name"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional description / question the spike aims to answer"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create_bug",
|
||||
"description": "Create a bug file in .story_kit/bugs/ with a deterministic filename and auto-commit to master. Returns the bug_id.",
|
||||
@@ -769,6 +787,8 @@ async fn handle_tools_call(
|
||||
"accept_story" => tool_accept_story(&args, ctx),
|
||||
// Story mutation tools (auto-commit to master)
|
||||
"check_criterion" => tool_check_criterion(&args, ctx),
|
||||
// Spike lifecycle tools
|
||||
"create_spike" => tool_create_spike(&args, ctx),
|
||||
// Bug lifecycle tools
|
||||
"create_bug" => tool_create_bug(&args, ctx),
|
||||
"list_bugs" => tool_list_bugs(ctx),
|
||||
@@ -1174,6 +1194,21 @@ fn tool_check_criterion(args: &Value, ctx: &AppContext) -> Result<String, String
|
||||
))
|
||||
}
|
||||
|
||||
// ── Spike lifecycle tool implementations ─────────────────────────
|
||||
|
||||
fn tool_create_spike(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
let name = args
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing required argument: name")?;
|
||||
let description = args.get("description").and_then(|v| v.as_str());
|
||||
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let spike_id = create_spike_file(&root, name, description)?;
|
||||
|
||||
Ok(format!("Created spike: {spike_id}"))
|
||||
}
|
||||
|
||||
// ── Bug lifecycle tool implementations ───────────────────────────
|
||||
|
||||
fn tool_create_bug(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
@@ -1499,13 +1534,14 @@ mod tests {
|
||||
assert!(!names.contains(&"report_completion"));
|
||||
assert!(names.contains(&"accept_story"));
|
||||
assert!(names.contains(&"check_criterion"));
|
||||
assert!(names.contains(&"create_spike"));
|
||||
assert!(names.contains(&"create_bug"));
|
||||
assert!(names.contains(&"list_bugs"));
|
||||
assert!(names.contains(&"close_bug"));
|
||||
assert!(names.contains(&"merge_agent_work"));
|
||||
assert!(names.contains(&"move_story_to_merge"));
|
||||
assert!(names.contains(&"request_qa"));
|
||||
assert_eq!(tools.len(), 25);
|
||||
assert_eq!(tools.len(), 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2010,6 +2046,77 @@ mod tests {
|
||||
assert!(tmp.path().join(".story_kit/work/5_archived/1_bug_crash.md").exists());
|
||||
}
|
||||
|
||||
// ── Spike lifecycle tool tests ─────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn create_spike_in_tools_list() {
|
||||
let resp = handle_tools_list(Some(json!(1)));
|
||||
let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
|
||||
let tool = tools.iter().find(|t| t["name"] == "create_spike");
|
||||
assert!(tool.is_some(), "create_spike missing from tools list");
|
||||
let t = tool.unwrap();
|
||||
assert!(t["description"].is_string());
|
||||
let required = t["inputSchema"]["required"].as_array().unwrap();
|
||||
let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
|
||||
assert!(req_names.contains(&"name"));
|
||||
// description is optional
|
||||
assert!(!req_names.contains(&"description"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_create_spike_missing_name() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_create_spike(&json!({}), &ctx);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_create_spike_rejects_empty_name() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_create_spike(&json!({"name": "!!!"}), &ctx);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("alphanumeric"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_create_spike_creates_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
|
||||
let result = tool_create_spike(
|
||||
&json!({"name": "Compare Encoders", "description": "Which encoder is fastest?"}),
|
||||
&ctx,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(result.contains("1_spike_compare_encoders"));
|
||||
let spike_file = tmp
|
||||
.path()
|
||||
.join(".story_kit/work/1_upcoming/1_spike_compare_encoders.md");
|
||||
assert!(spike_file.exists());
|
||||
let contents = std::fs::read_to_string(&spike_file).unwrap();
|
||||
assert!(contents.starts_with("---\nname: \"Compare Encoders\"\n---"));
|
||||
assert!(contents.contains("Which encoder is fastest?"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_create_spike_creates_file_without_description() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
|
||||
let result = tool_create_spike(&json!({"name": "My Spike"}), &ctx).unwrap();
|
||||
assert!(result.contains("1_spike_my_spike"));
|
||||
|
||||
let spike_file = tmp.path().join(".story_kit/work/1_upcoming/1_spike_my_spike.md");
|
||||
assert!(spike_file.exists());
|
||||
let contents = std::fs::read_to_string(&spike_file).unwrap();
|
||||
assert!(contents.starts_with("---\nname: \"My Spike\"\n---"));
|
||||
assert!(contents.contains("## Question\n\n- TBD\n"));
|
||||
}
|
||||
|
||||
// ── Mergemaster tool tests ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user