From bd2414437ae0ad7d0f5ca73461060cedb28bbd2c Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 12:12:20 +0000 Subject: [PATCH] story-kit: start 63_story_auto_spawn_mergemaster_on_merge --- ...3_story_auto_spawn_mergemaster_on_merge.md | 24 ++++++++ server/src/http/mcp.rs | 55 ++++++++++++++----- 2 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 .story_kit/work/2_current/63_story_auto_spawn_mergemaster_on_merge.md diff --git a/.story_kit/work/2_current/63_story_auto_spawn_mergemaster_on_merge.md b/.story_kit/work/2_current/63_story_auto_spawn_mergemaster_on_merge.md new file mode 100644 index 0000000..da42870 --- /dev/null +++ b/.story_kit/work/2_current/63_story_auto_spawn_mergemaster_on_merge.md @@ -0,0 +1,24 @@ +--- +name: Auto-Spawn Mergemaster on Move to Merge +test_plan: pending +--- + +# Story 63: Auto-Spawn Mergemaster on Move to Merge + +## User Story + +As an agent completing work on a story, when I call `move_story_to_merge`, the mergemaster agent should start automatically so I don't need to know how to spawn agents or understand the pipeline topology. + +## Acceptance Criteria + +- [ ] `move_story_to_merge` MCP tool moves the story file to `work/4_merge/` AND starts the mergemaster agent on that story's worktree (same pattern as `request_qa`) +- [ ] The tool returns JSON with story_id, agent_name, status, worktree_path, and a message (matching `request_qa` response shape) +- [ ] The tool accepts an optional `agent_name` parameter, defaulting to `"mergemaster"` +- [ ] The tool description is updated to reflect that it spawns the mergemaster agent +- [ ] Existing tests are updated to reflect the new async behavior + +## Out of Scope + +- Changing the `request_qa` tool +- Adding new pipeline stages +- Changing how `start_agent` works internally diff --git a/server/src/http/mcp.rs b/server/src/http/mcp.rs index fe8a8b8..82f182d 100644 --- a/server/src/http/mcp.rs +++ b/server/src/http/mcp.rs @@ -731,13 +731,17 @@ fn handle_tools_list(id: Option) -> JsonRpcResponse { }, { "name": "move_story_to_merge", - "description": "Move a story or bug from work/2_current/ to work/4_merge/ to queue it for the mergemaster pipeline. Auto-commits to master.", + "description": "Move a story or bug from work/2_current/ to work/4_merge/ to queue it for the mergemaster pipeline and automatically spawn the mergemaster agent to squash-merge, run quality gates, and archive.", "inputSchema": { "type": "object", "properties": { "story_id": { "type": "string", "description": "Story identifier (filename stem, e.g. '28_my_story')" + }, + "agent_name": { + "type": "string", + "description": "Agent name to use for merging (defaults to 'mergemaster')" } }, "required": ["story_id"] @@ -814,7 +818,7 @@ async fn handle_tools_call( "close_bug" => tool_close_bug(&args, ctx), // Mergemaster tools "merge_agent_work" => tool_merge_agent_work(&args, ctx).await, - "move_story_to_merge" => tool_move_story_to_merge(&args, ctx), + "move_story_to_merge" => tool_move_story_to_merge(&args, ctx).await, // QA tools "request_qa" => tool_request_qa(&args, ctx).await, _ => Err(format!("Unknown tool: {tool_name}")), @@ -1367,18 +1371,38 @@ async fn tool_merge_agent_work(args: &Value, ctx: &AppContext) -> Result Result { +async fn tool_move_story_to_merge(args: &Value, ctx: &AppContext) -> Result { let story_id = args .get("story_id") .and_then(|v| v.as_str()) .ok_or("Missing required argument: story_id")?; + let agent_name = args + .get("agent_name") + .and_then(|v| v.as_str()) + .unwrap_or("mergemaster"); let project_root = ctx.agents.get_project_root(&ctx.state)?; + + // Move story from work/2_current/ to work/4_merge/ move_story_to_merge(&project_root, story_id)?; - Ok(format!( - "Story '{story_id}' moved to work/4_merge/ and committed. Ready for mergemaster." - )) + // Start the mergemaster agent on the story worktree + let info = ctx + .agents + .start_agent(&project_root, story_id, Some(agent_name)) + .await?; + + serde_json::to_string_pretty(&json!({ + "story_id": info.story_id, + "agent_name": info.agent_name, + "status": info.status.to_string(), + "worktree_path": info.worktree_path, + "message": format!( + "Story '{story_id}' moved to work/4_merge/ and mergemaster agent '{}' started.", + info.agent_name + ), + })) + .map_err(|e| format!("Serialization error: {e}")) } // ── QA tool implementations ─────────────────────────────────────── @@ -2175,6 +2199,8 @@ mod tests { 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(&"story_id")); + // agent_name is optional + assert!(!req_names.contains(&"agent_name")); } #[tokio::test] @@ -2186,17 +2212,17 @@ mod tests { assert!(result.unwrap_err().contains("story_id")); } - #[test] - fn tool_move_story_to_merge_missing_story_id() { + #[tokio::test] + async fn tool_move_story_to_merge_missing_story_id() { let tmp = tempfile::tempdir().unwrap(); let ctx = test_ctx(tmp.path()); - let result = tool_move_story_to_merge(&json!({}), &ctx); + let result = tool_move_story_to_merge(&json!({}), &ctx).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("story_id")); } - #[test] - fn tool_move_story_to_merge_moves_file() { + #[tokio::test] + async fn tool_move_story_to_merge_moves_file() { let tmp = tempfile::tempdir().unwrap(); setup_git_repo_in(tmp.path()); let current_dir = tmp.path().join(".story_kit/work/2_current"); @@ -2215,13 +2241,16 @@ mod tests { .unwrap(); let ctx = test_ctx(tmp.path()); - let result = tool_move_story_to_merge(&json!({"story_id": "24_story_test"}), &ctx).unwrap(); - assert!(result.contains("4_merge")); + // The agent start will fail in test (no worktree/config), but the file move should succeed + let result = tool_move_story_to_merge(&json!({"story_id": "24_story_test"}), &ctx).await; + // File should have been moved regardless of agent start outcome assert!(!story_file.exists(), "2_current file should be gone"); assert!( tmp.path().join(".story_kit/work/4_merge/24_story_test.md").exists(), "4_merge file should exist" ); + // Result is either Ok (agent started) or Err (agent failed - acceptable in tests) + let _ = result; } #[tokio::test]