story-kit: start 59_story_current_work_panel
This commit is contained in:
@@ -1,14 +1,26 @@
|
||||
use crate::agents::AgentStatus;
|
||||
use crate::http::context::AppContext;
|
||||
use crate::io::story_metadata::parse_front_matter;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Agent assignment embedded in a pipeline stage item.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AgentAssignment {
|
||||
pub agent_name: String,
|
||||
pub model: Option<String>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct UpcomingStory {
|
||||
pub story_id: String,
|
||||
pub name: Option<String>,
|
||||
pub error: Option<String>,
|
||||
/// Active agent working on this item, if any.
|
||||
pub agent: Option<AgentAssignment>,
|
||||
}
|
||||
|
||||
pub struct StoryValidationResult {
|
||||
@@ -28,16 +40,55 @@ pub struct PipelineState {
|
||||
|
||||
/// Load the full pipeline state (all 4 active stages).
|
||||
pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
let agent_map = build_active_agent_map(ctx);
|
||||
Ok(PipelineState {
|
||||
upcoming: load_stage_items(ctx, "1_upcoming")?,
|
||||
current: load_stage_items(ctx, "2_current")?,
|
||||
qa: load_stage_items(ctx, "3_qa")?,
|
||||
merge: load_stage_items(ctx, "4_merge")?,
|
||||
upcoming: load_stage_items(ctx, "1_upcoming", &HashMap::new())?,
|
||||
current: load_stage_items(ctx, "2_current", &agent_map)?,
|
||||
qa: load_stage_items(ctx, "3_qa", &agent_map)?,
|
||||
merge: load_stage_items(ctx, "4_merge", &agent_map)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a map from story_id → AgentAssignment for all pending/running agents.
|
||||
fn build_active_agent_map(ctx: &AppContext) -> HashMap<String, AgentAssignment> {
|
||||
let agents = match ctx.agents.list_agents() {
|
||||
Ok(a) => a,
|
||||
Err(_) => return HashMap::new(),
|
||||
};
|
||||
|
||||
let config_opt = ctx
|
||||
.state
|
||||
.get_project_root()
|
||||
.ok()
|
||||
.and_then(|root| crate::config::ProjectConfig::load(&root).ok());
|
||||
|
||||
let mut map = HashMap::new();
|
||||
for agent in agents {
|
||||
if !matches!(agent.status, AgentStatus::Pending | AgentStatus::Running) {
|
||||
continue;
|
||||
}
|
||||
let model = config_opt
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.find_agent(&agent.agent_name))
|
||||
.and_then(|ac| ac.model.clone());
|
||||
map.insert(
|
||||
agent.story_id.clone(),
|
||||
AgentAssignment {
|
||||
agent_name: agent.agent_name,
|
||||
model,
|
||||
status: agent.status.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Load work items from any pipeline stage directory.
|
||||
fn load_stage_items(ctx: &AppContext, stage_dir: &str) -> Result<Vec<UpcomingStory>, String> {
|
||||
fn load_stage_items(
|
||||
ctx: &AppContext,
|
||||
stage_dir: &str,
|
||||
agent_map: &HashMap<String, AgentAssignment>,
|
||||
) -> Result<Vec<UpcomingStory>, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let dir = root.join(".story_kit").join("work").join(stage_dir);
|
||||
|
||||
@@ -65,7 +116,8 @@ fn load_stage_items(ctx: &AppContext, stage_dir: &str) -> Result<Vec<UpcomingSto
|
||||
Ok(meta) => (meta.name, None),
|
||||
Err(e) => (None, Some(e.to_string())),
|
||||
};
|
||||
stories.push(UpcomingStory { story_id, name, error });
|
||||
let agent = agent_map.get(&story_id).cloned();
|
||||
stories.push(UpcomingStory { story_id, name, error, agent });
|
||||
}
|
||||
|
||||
stories.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
@@ -73,7 +125,7 @@ fn load_stage_items(ctx: &AppContext, stage_dir: &str) -> Result<Vec<UpcomingSto
|
||||
}
|
||||
|
||||
pub fn load_upcoming_stories(ctx: &AppContext) -> Result<Vec<UpcomingStory>, String> {
|
||||
load_stage_items(ctx, "1_upcoming")
|
||||
load_stage_items(ctx, "1_upcoming", &HashMap::new())
|
||||
}
|
||||
|
||||
/// Shared create-story logic used by both the OpenApi and MCP handlers.
|
||||
@@ -546,6 +598,81 @@ mod tests {
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline_state_includes_agent_for_running_story() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let current = root.join(".story_kit/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("10_story_test.md"),
|
||||
"---\nname: Test Story\ntest_plan: approved\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ctx = crate::http::context::AppContext::new_test(root);
|
||||
ctx.agents.inject_test_agent("10_story_test", "coder-1", crate::agents::AgentStatus::Running);
|
||||
|
||||
let state = load_pipeline_state(&ctx).unwrap();
|
||||
|
||||
assert_eq!(state.current.len(), 1);
|
||||
let item = &state.current[0];
|
||||
assert!(item.agent.is_some(), "running agent should appear on work item");
|
||||
let agent = item.agent.as_ref().unwrap();
|
||||
assert_eq!(agent.agent_name, "coder-1");
|
||||
assert_eq!(agent.status, "running");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline_state_no_agent_for_completed_story() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let current = root.join(".story_kit/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("11_story_done.md"),
|
||||
"---\nname: Done Story\ntest_plan: approved\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ctx = crate::http::context::AppContext::new_test(root);
|
||||
ctx.agents.inject_test_agent("11_story_done", "coder-1", crate::agents::AgentStatus::Completed);
|
||||
|
||||
let state = load_pipeline_state(&ctx).unwrap();
|
||||
|
||||
assert_eq!(state.current.len(), 1);
|
||||
assert!(
|
||||
state.current[0].agent.is_none(),
|
||||
"completed agent should not appear on work item"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline_state_pending_agent_included() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
let current = root.join(".story_kit/work/2_current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("12_story_pending.md"),
|
||||
"---\nname: Pending Story\ntest_plan: approved\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ctx = crate::http::context::AppContext::new_test(root);
|
||||
ctx.agents.inject_test_agent("12_story_pending", "coder-1", crate::agents::AgentStatus::Pending);
|
||||
|
||||
let state = load_pipeline_state(&ctx).unwrap();
|
||||
|
||||
assert_eq!(state.current.len(), 1);
|
||||
let item = &state.current[0];
|
||||
assert!(item.agent.is_some(), "pending agent should appear on work item");
|
||||
assert_eq!(item.agent.as_ref().unwrap().status, "pending");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_upcoming_parses_metadata() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user