diff --git a/.story_kit/work/2_current/274_story_mcp_pipeline_status_tool_with_agent_assignments.md b/.story_kit/work/2_current/274_story_mcp_pipeline_status_tool_with_agent_assignments.md new file mode 100644 index 0000000..ed60c18 --- /dev/null +++ b/.story_kit/work/2_current/274_story_mcp_pipeline_status_tool_with_agent_assignments.md @@ -0,0 +1,20 @@ +--- +name: "MCP pipeline status tool with agent assignments" +--- + +# Story 274: MCP pipeline status tool with agent assignments + +## User Story + +As a user checking pipeline status, I want an MCP tool that returns a structured status report including which agent is assigned to each work item, so that I can quickly see what's active and spot stuck items. + +## Acceptance Criteria + +- [ ] New MCP tool (e.g. `get_pipeline_status`) returns all work items across all active pipeline stages (current, qa, merge, done) with their stage, name, and assigned agent +- [ ] Upcoming backlog items are included with count or listing +- [ ] Agent assignment info comes from story front matter (`agent` field) and/or the running agent list +- [ ] Response is structured/deterministic (not free-form prose) + +## Out of Scope + +- TBD diff --git a/server/src/http/mcp.rs b/server/src/http/mcp.rs index 2b8b0b2..e1fc51a 100644 --- a/server/src/http/mcp.rs +++ b/server/src/http/mcp.rs @@ -8,7 +8,7 @@ use crate::http::settings::get_editor_command_from_store; use crate::http::workflow::{ add_criterion_to_file, check_criterion_in_file, create_bug_file, create_refactor_file, create_spike_file, create_story_file, list_bug_files, list_refactor_files, - load_upcoming_stories, update_story_in_file, validate_story_dirs, + load_pipeline_state, load_upcoming_stories, update_story_in_file, validate_story_dirs, }; use crate::worktree; use crate::io::story_metadata::{parse_front_matter, parse_unchecked_todos, write_merge_failure}; @@ -862,6 +862,14 @@ fn handle_tools_list(id: Option) -> JsonRpcResponse { "required": ["story_id"] } }, + { + "name": "get_pipeline_status", + "description": "Return a structured snapshot of the full work item pipeline. Includes all active stages (current, qa, merge, done) with each item's stage, name, and assigned agent. Also includes upcoming backlog items.", + "inputSchema": { + "type": "object", + "properties": {} + } + }, { "name": "get_server_logs", "description": "Return recent server log lines captured in the in-process ring buffer. Useful for diagnosing runtime behaviour such as WebSocket events, MCP call flow, and filesystem watcher activity.", @@ -963,6 +971,8 @@ async fn handle_tools_call( "report_merge_failure" => tool_report_merge_failure(&args, ctx), // QA tools "request_qa" => tool_request_qa(&args, ctx).await, + // Pipeline status + "get_pipeline_status" => tool_get_pipeline_status(ctx), // Diagnostics "get_server_logs" => tool_get_server_logs(&args), // Permission bridge (Claude Code → frontend dialog) @@ -1044,6 +1054,47 @@ fn tool_list_upcoming(ctx: &AppContext) -> Result { .map_err(|e| format!("Serialization error: {e}")) } +fn tool_get_pipeline_status(ctx: &AppContext) -> Result { + let state = load_pipeline_state(ctx)?; + + fn map_items(items: &[crate::http::workflow::UpcomingStory], stage: &str) -> Vec { + items + .iter() + .map(|s| { + json!({ + "story_id": s.story_id, + "name": s.name, + "stage": stage, + "agent": s.agent.as_ref().map(|a| json!({ + "agent_name": a.agent_name, + "model": a.model, + "status": a.status, + })), + }) + }) + .collect() + } + + let mut active: Vec = Vec::new(); + active.extend(map_items(&state.current, "current")); + active.extend(map_items(&state.qa, "qa")); + active.extend(map_items(&state.merge, "merge")); + active.extend(map_items(&state.done, "done")); + + let upcoming: Vec = state + .upcoming + .iter() + .map(|s| json!({ "story_id": s.story_id, "name": s.name })) + .collect(); + + serde_json::to_string_pretty(&json!({ + "active": active, + "upcoming": upcoming, + "upcoming_count": upcoming.len(), + })) + .map_err(|e| format!("Serialization error: {e}")) +} + fn tool_get_story_todos(args: &Value, ctx: &AppContext) -> Result { let story_id = args .get("story_id") @@ -2230,7 +2281,8 @@ mod tests { assert!(names.contains(&"request_qa")); assert!(names.contains(&"get_server_logs")); assert!(names.contains(&"prompt_permission")); - assert_eq!(tools.len(), 34); + assert!(names.contains(&"get_pipeline_status")); + assert_eq!(tools.len(), 35); } #[test] @@ -2297,6 +2349,81 @@ mod tests { assert!(result.unwrap_err().contains("Missing required argument")); } + #[test] + fn tool_get_pipeline_status_returns_structured_response() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + + for (stage, id, name) in &[ + ("1_upcoming", "10_story_upcoming", "Upcoming Story"), + ("2_current", "20_story_current", "Current Story"), + ("3_qa", "30_story_qa", "QA Story"), + ("4_merge", "40_story_merge", "Merge Story"), + ("5_done", "50_story_done", "Done Story"), + ] { + let dir = root.join(".story_kit/work").join(stage); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write( + dir.join(format!("{id}.md")), + format!("---\nname: \"{name}\"\n---\n"), + ) + .unwrap(); + } + + let ctx = test_ctx(root); + let result = tool_get_pipeline_status(&ctx).unwrap(); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + // Active stages include current, qa, merge, done + let active = parsed["active"].as_array().unwrap(); + assert_eq!(active.len(), 4); + + let stages: Vec<&str> = active.iter().map(|i| i["stage"].as_str().unwrap()).collect(); + assert!(stages.contains(&"current")); + assert!(stages.contains(&"qa")); + assert!(stages.contains(&"merge")); + assert!(stages.contains(&"done")); + + // Upcoming backlog + let upcoming = parsed["upcoming"].as_array().unwrap(); + assert_eq!(upcoming.len(), 1); + assert_eq!(upcoming[0]["story_id"], "10_story_upcoming"); + assert_eq!(parsed["upcoming_count"], 1); + } + + #[test] + fn tool_get_pipeline_status_includes_agent_assignment() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + + let current = root.join(".story_kit/work/2_current"); + std::fs::create_dir_all(¤t).unwrap(); + std::fs::write( + current.join("20_story_active.md"), + "---\nname: \"Active Story\"\n---\n", + ) + .unwrap(); + + let ctx = test_ctx(root); + ctx.agents.inject_test_agent( + "20_story_active", + "coder-1", + crate::agents::AgentStatus::Running, + ); + + let result = tool_get_pipeline_status(&ctx).unwrap(); + let parsed: Value = serde_json::from_str(&result).unwrap(); + + let active = parsed["active"].as_array().unwrap(); + assert_eq!(active.len(), 1); + let item = &active[0]; + assert_eq!(item["story_id"], "20_story_active"); + assert_eq!(item["stage"], "current"); + assert!(!item["agent"].is_null(), "agent should be present"); + assert_eq!(item["agent"]["agent_name"], "coder-1"); + assert_eq!(item["agent"]["status"], "running"); + } + #[test] fn tool_get_story_todos_missing_file() { let tmp = tempfile::tempdir().unwrap();