story-kit: merge 342_story_web_ui_button_to_delete_a_story_from_the_pipeline

This commit is contained in:
Dave
2026-03-20 09:10:56 +00:00
parent 3cfe25f97a
commit 60e1d7bf64
5 changed files with 180 additions and 6 deletions

View File

@@ -967,6 +967,20 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
}
}
},
{
"name": "delete_story",
"description": "Delete a work item from the pipeline entirely. Stops any running agent, removes the worktree, and deletes the story file. Use only for removing obsolete or duplicate items.",
"inputSchema": {
"type": "object",
"properties": {
"story_id": {
"type": "string",
"description": "Work item identifier (filename stem, e.g. '28_story_my_feature')"
}
},
"required": ["story_id"]
}
},
{
"name": "move_story",
"description": "Move a work item (story, bug, spike, or refactor) to an arbitrary pipeline stage. Prefer dedicated tools when available: use accept_story to mark items done, move_story_to_merge to queue for merging, or request_qa to trigger QA review. Use move_story only for arbitrary moves that lack a dedicated tool — for example, moving a story back to backlog or recovering a ghost story by moving it back to current.",
@@ -1061,6 +1075,8 @@ async fn handle_tools_call(
"prompt_permission" => diagnostics::tool_prompt_permission(&args, ctx).await,
// Token usage
"get_token_usage" => diagnostics::tool_get_token_usage(&args, ctx),
// Delete story
"delete_story" => story_tools::tool_delete_story(&args, ctx).await,
// Arbitrary pipeline movement
"move_story" => diagnostics::tool_move_story(&args, ctx),
_ => Err(format!("Unknown tool: {tool_name}")),
@@ -1171,7 +1187,8 @@ mod tests {
assert!(names.contains(&"rebuild_and_restart"));
assert!(names.contains(&"get_token_usage"));
assert!(names.contains(&"move_story"));
assert_eq!(tools.len(), 41);
assert!(names.contains(&"delete_story"));
assert_eq!(tools.len(), 42);
}
#[test]

View File

@@ -390,6 +390,53 @@ pub(super) fn tool_close_bug(args: &Value, ctx: &AppContext) -> Result<String, S
))
}
pub(super) async fn tool_delete_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
let story_id = args
.get("story_id")
.and_then(|v| v.as_str())
.ok_or("Missing required argument: story_id")?;
let project_root = ctx.agents.get_project_root(&ctx.state)?;
// 1. Stop any running agents for this story (best-effort)
if let Ok(agents) = ctx.agents.list_agents() {
for agent in agents.iter().filter(|a| a.story_id == story_id) {
let _ = ctx.agents.stop_agent(&project_root, story_id, &agent.agent_name).await;
}
}
// 2. Remove agent pool entries
ctx.agents.remove_agents_for_story(story_id);
// 3. Remove worktree (best-effort)
if let Ok(config) = crate::config::ProjectConfig::load(&project_root) {
let _ = crate::worktree::remove_worktree_by_story_id(&project_root, story_id, &config).await;
}
// 4. Find and delete the story file from any pipeline stage
let sk = project_root.join(".story_kit").join("work");
let stage_dirs = ["1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived"];
let mut deleted = false;
for stage in &stage_dirs {
let path = sk.join(stage).join(format!("{story_id}.md"));
if path.exists() {
fs::remove_file(&path)
.map_err(|e| format!("Failed to delete story file: {e}"))?;
slog_warn!("[delete_story] Deleted '{story_id}' from work/{stage}/");
deleted = true;
break;
}
}
if !deleted {
return Err(format!(
"Story '{story_id}' not found in any pipeline stage."
));
}
Ok(format!("Story '{story_id}' deleted from pipeline."))
}
pub(super) fn tool_create_refactor(args: &Value, ctx: &AppContext) -> Result<String, String> {
let name = args
.get("name")
@@ -1129,6 +1176,52 @@ mod tests {
assert!(result.unwrap_err().contains("blocked"));
}
#[tokio::test]
async fn tool_delete_story_missing_story_id() {
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_delete_story(&json!({}), &ctx).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("story_id"));
}
#[tokio::test]
async fn tool_delete_story_not_found_returns_error() {
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_delete_story(&json!({"story_id": "99_nonexistent"}), &ctx).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("not found in any pipeline stage"));
}
#[tokio::test]
async fn tool_delete_story_deletes_file_from_backlog() {
let tmp = tempfile::tempdir().unwrap();
let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&backlog).unwrap();
let story_file = backlog.join("10_story_cleanup.md");
fs::write(&story_file, "---\nname: Cleanup\n---\n").unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_delete_story(&json!({"story_id": "10_story_cleanup"}), &ctx).await;
assert!(result.is_ok(), "expected ok: {result:?}");
assert!(!story_file.exists(), "story file should be deleted");
}
#[tokio::test]
async fn tool_delete_story_deletes_file_from_current() {
let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".story_kit/work/2_current");
fs::create_dir_all(&current).unwrap();
let story_file = current.join("11_story_active.md");
fs::write(&story_file, "---\nname: Active\n---\n").unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_delete_story(&json!({"story_id": "11_story_active"}), &ctx).await;
assert!(result.is_ok(), "expected ok: {result:?}");
assert!(!story_file.exists(), "story file should be deleted");
}
#[test]
fn tool_accept_story_missing_story_id() {
let tmp = tempfile::tempdir().unwrap();