story-kit: start 63_story_auto_spawn_mergemaster_on_merge
This commit is contained in:
@@ -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
|
||||||
@@ -731,13 +731,17 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "move_story_to_merge",
|
"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": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"story_id": {
|
"story_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Story identifier (filename stem, e.g. '28_my_story')"
|
"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"]
|
"required": ["story_id"]
|
||||||
@@ -814,7 +818,7 @@ async fn handle_tools_call(
|
|||||||
"close_bug" => tool_close_bug(&args, ctx),
|
"close_bug" => tool_close_bug(&args, ctx),
|
||||||
// Mergemaster tools
|
// Mergemaster tools
|
||||||
"merge_agent_work" => tool_merge_agent_work(&args, ctx).await,
|
"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
|
// QA tools
|
||||||
"request_qa" => tool_request_qa(&args, ctx).await,
|
"request_qa" => tool_request_qa(&args, ctx).await,
|
||||||
_ => Err(format!("Unknown tool: {tool_name}")),
|
_ => Err(format!("Unknown tool: {tool_name}")),
|
||||||
@@ -1367,18 +1371,38 @@ async fn tool_merge_agent_work(args: &Value, ctx: &AppContext) -> Result<String,
|
|||||||
.map_err(|e| format!("Serialization error: {e}"))
|
.map_err(|e| format!("Serialization error: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool_move_story_to_merge(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
async fn tool_move_story_to_merge(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
let story_id = args
|
let story_id = args
|
||||||
.get("story_id")
|
.get("story_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or("Missing required argument: story_id")?;
|
.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)?;
|
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)?;
|
move_story_to_merge(&project_root, story_id)?;
|
||||||
|
|
||||||
Ok(format!(
|
// Start the mergemaster agent on the story worktree
|
||||||
"Story '{story_id}' moved to work/4_merge/ and committed. Ready for mergemaster."
|
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 ───────────────────────────────────────
|
// ── QA tool implementations ───────────────────────────────────────
|
||||||
@@ -2175,6 +2199,8 @@ mod tests {
|
|||||||
let required = t["inputSchema"]["required"].as_array().unwrap();
|
let required = t["inputSchema"]["required"].as_array().unwrap();
|
||||||
let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
|
let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
|
||||||
assert!(req_names.contains(&"story_id"));
|
assert!(req_names.contains(&"story_id"));
|
||||||
|
// agent_name is optional
|
||||||
|
assert!(!req_names.contains(&"agent_name"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -2186,17 +2212,17 @@ mod tests {
|
|||||||
assert!(result.unwrap_err().contains("story_id"));
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn tool_move_story_to_merge_missing_story_id() {
|
async fn tool_move_story_to_merge_missing_story_id() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let ctx = test_ctx(tmp.path());
|
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.is_err());
|
||||||
assert!(result.unwrap_err().contains("story_id"));
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn tool_move_story_to_merge_moves_file() {
|
async fn tool_move_story_to_merge_moves_file() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
setup_git_repo_in(tmp.path());
|
setup_git_repo_in(tmp.path());
|
||||||
let current_dir = tmp.path().join(".story_kit/work/2_current");
|
let current_dir = tmp.path().join(".story_kit/work/2_current");
|
||||||
@@ -2215,13 +2241,16 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let ctx = test_ctx(tmp.path());
|
let ctx = test_ctx(tmp.path());
|
||||||
let result = tool_move_story_to_merge(&json!({"story_id": "24_story_test"}), &ctx).unwrap();
|
// The agent start will fail in test (no worktree/config), but the file move should succeed
|
||||||
assert!(result.contains("4_merge"));
|
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!(!story_file.exists(), "2_current file should be gone");
|
||||||
assert!(
|
assert!(
|
||||||
tmp.path().join(".story_kit/work/4_merge/24_story_test.md").exists(),
|
tmp.path().join(".story_kit/work/4_merge/24_story_test.md").exists(),
|
||||||
"4_merge file should exist"
|
"4_merge file should exist"
|
||||||
);
|
);
|
||||||
|
// Result is either Ok (agent started) or Err (agent failed - acceptable in tests)
|
||||||
|
let _ = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in New Issue
Block a user