diff --git a/server/src/http/mcp/story_tools/story/query.rs b/server/src/http/mcp/story_tools/story/query.rs index 78856bc2..a70ec725 100644 --- a/server/src/http/mcp/story_tools/story/query.rs +++ b/server/src/http/mcp/story_tools/story/query.rs @@ -39,13 +39,17 @@ pub(crate) fn tool_get_pipeline_status(ctx: &AppContext) -> Result &str { + crate::chat::util::truncate_at_char_boundary(name, 120) + } + fn map_items(items: &[crate::http::workflow::UpcomingStory], stage: &str) -> Vec { items .iter() .map(|s| { let mut item = json!({ "story_id": s.story_id, - "name": s.name, + "name": slim_name(&s.name), "stage": stage, "agent": s.agent.as_ref().map(|a| json!({ "agent_name": a.agent_name, @@ -53,20 +57,12 @@ pub(crate) fn tool_get_pipeline_status(ctx: &AppContext) -> Result Result = state .backlog .iter() - .map(|s| { - let mut item = json!({ "story_id": s.story_id, "name": s.name }); - if let Some(ref epic_id) = s.epic_id { - item["epic_id"] = json!(epic_id); - } - item - }) + .map(|s| json!({ "story_id": s.story_id, "name": slim_name(&s.name) })) .collect(); let archived: Vec = state .archived .iter() - .map(|s| json!({ "story_id": s.story_id, "name": s.name, "stage": "archived" })) + .map(|s| json!({ "story_id": s.story_id, "name": slim_name(&s.name), "stage": "archived" })) .collect(); serde_json::to_string_pretty(&json!({ @@ -248,6 +238,82 @@ mod tests { assert_eq!(item["valid"], true); } + #[test] + fn pipeline_status_50_items_under_10kb() { + crate::db::ensure_content_store(); + let stages = [ + ("1_backlog", "backlog"), + ("2_current", "current"), + ("3_qa", "qa"), + ("4_merge", "merge"), + ("5_done", "done"), + ]; + for (i, (dir, _)) in stages.iter().enumerate() { + for j in 0..10 { + let id = format!("99{i}{j}0_story_size_test"); + let name = format!("Pipeline Size Test Story {i}-{j}"); + crate::db::write_item_with_content( + &id, + dir, + &format!("---\nname: \"{name}\"\n---\n"), + crate::db::ItemMeta { + name: Some(name), + ..Default::default() + }, + ); + } + } + let tmp = tempfile::tempdir().unwrap(); + let ctx = test_ctx(tmp.path()); + let result = tool_get_pipeline_status(&ctx).unwrap(); + assert!( + result.len() < 10 * 1024, + "50-item response must be under 10 KB; got {} bytes", + result.len() + ); + } + + #[test] + fn pipeline_status_per_item_under_500_bytes() { + crate::db::ensure_content_store(); + // Insert one item per active stage with a moderately long name. + let stages = [ + ("2_current", "9995_story_peritem_current"), + ("3_qa", "9996_story_peritem_qa"), + ("4_merge", "9997_story_peritem_merge"), + ("5_done", "9998_story_peritem_done"), + ]; + for (dir, id) in &stages { + let name = "A Reasonably Named Story For Size Testing"; + crate::db::write_item_with_content( + id, + dir, + &format!("---\nname: \"{name}\"\n---\n"), + crate::db::ItemMeta { + name: Some(name.to_string()), + ..Default::default() + }, + ); + } + let tmp = tempfile::tempdir().unwrap(); + let ctx = test_ctx(tmp.path()); + let result = tool_get_pipeline_status(&ctx).unwrap(); + let parsed: Value = serde_json::from_str(&result).unwrap(); + let active = parsed["active"].as_array().unwrap(); + for item in active { + if stages.iter().any(|(_, id)| item["story_id"] == *id) { + let item_json = serde_json::to_string(item).unwrap(); + assert!( + item_json.len() < 500, + "per-item payload must be under 500 bytes; story_id={} got {} bytes: {}", + item["story_id"], + item_json.len(), + item_json + ); + } + } + } + #[test] fn tool_validate_stories_with_invalid_front_matter() { let tmp = tempfile::tempdir().unwrap(); diff --git a/server/src/http/mcp/tools_list/story_tools.rs b/server/src/http/mcp/tools_list/story_tools.rs index 2529115e..6148278c 100644 --- a/server/src/http/mcp/tools_list/story_tools.rs +++ b/server/src/http/mcp/tools_list/story_tools.rs @@ -574,7 +574,7 @@ pub(super) fn story_tools() -> Vec { }), json!({ "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.", + "description": "Return a structured snapshot of the full work item pipeline. Each item includes only slim fields: story_id, name (capped at 120 chars), stage, agent (with agent_name/model/status), and optional boolean flags blocked and retry_count. Active stages (current, qa, merge, done) appear in the 'active' array; backlog items in 'backlog'. For full story details, use status(story_id) or dump_crdt.", "inputSchema": { "type": "object", "properties": {}