story-kit: start 274_story_mcp_pipeline_status_tool_with_agent_assignments
This commit is contained in:
@@ -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
|
||||||
@@ -8,7 +8,7 @@ use crate::http::settings::get_editor_command_from_store;
|
|||||||
use crate::http::workflow::{
|
use crate::http::workflow::{
|
||||||
add_criterion_to_file, check_criterion_in_file, create_bug_file, create_refactor_file,
|
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,
|
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::worktree;
|
||||||
use crate::io::story_metadata::{parse_front_matter, parse_unchecked_todos, write_merge_failure};
|
use crate::io::story_metadata::{parse_front_matter, parse_unchecked_todos, write_merge_failure};
|
||||||
@@ -862,6 +862,14 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
|||||||
"required": ["story_id"]
|
"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",
|
"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.",
|
"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),
|
"report_merge_failure" => tool_report_merge_failure(&args, ctx),
|
||||||
// QA tools
|
// QA tools
|
||||||
"request_qa" => tool_request_qa(&args, ctx).await,
|
"request_qa" => tool_request_qa(&args, ctx).await,
|
||||||
|
// Pipeline status
|
||||||
|
"get_pipeline_status" => tool_get_pipeline_status(ctx),
|
||||||
// Diagnostics
|
// Diagnostics
|
||||||
"get_server_logs" => tool_get_server_logs(&args),
|
"get_server_logs" => tool_get_server_logs(&args),
|
||||||
// Permission bridge (Claude Code → frontend dialog)
|
// Permission bridge (Claude Code → frontend dialog)
|
||||||
@@ -1044,6 +1054,47 @@ fn tool_list_upcoming(ctx: &AppContext) -> Result<String, String> {
|
|||||||
.map_err(|e| format!("Serialization error: {e}"))
|
.map_err(|e| format!("Serialization error: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tool_get_pipeline_status(ctx: &AppContext) -> Result<String, String> {
|
||||||
|
let state = load_pipeline_state(ctx)?;
|
||||||
|
|
||||||
|
fn map_items(items: &[crate::http::workflow::UpcomingStory], stage: &str) -> Vec<Value> {
|
||||||
|
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<Value> = 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<Value> = 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<String, String> {
|
fn tool_get_story_todos(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
let story_id = args
|
let story_id = args
|
||||||
.get("story_id")
|
.get("story_id")
|
||||||
@@ -2230,7 +2281,8 @@ mod tests {
|
|||||||
assert!(names.contains(&"request_qa"));
|
assert!(names.contains(&"request_qa"));
|
||||||
assert!(names.contains(&"get_server_logs"));
|
assert!(names.contains(&"get_server_logs"));
|
||||||
assert!(names.contains(&"prompt_permission"));
|
assert!(names.contains(&"prompt_permission"));
|
||||||
assert_eq!(tools.len(), 34);
|
assert!(names.contains(&"get_pipeline_status"));
|
||||||
|
assert_eq!(tools.len(), 35);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2297,6 +2349,81 @@ mod tests {
|
|||||||
assert!(result.unwrap_err().contains("Missing required argument"));
|
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]
|
#[test]
|
||||||
fn tool_get_story_todos_missing_file() {
|
fn tool_get_story_todos_missing_file() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user