huskies: merge 875
This commit is contained in:
@@ -2,9 +2,7 @@
|
||||
|
||||
use crate::agents::{feature_branch_has_unmerged_changes, move_story_to_done};
|
||||
use crate::http::context::AppContext;
|
||||
use crate::slog_warn;
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
|
||||
pub(crate) fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
let story_id = args
|
||||
@@ -39,122 +37,19 @@ pub(crate) async fn tool_delete_story(args: &Value, ctx: &AppContext) -> Result<
|
||||
.ok_or("Missing required argument: story_id")?;
|
||||
|
||||
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
||||
let mut failed_steps: Vec<String> = Vec::new();
|
||||
|
||||
// 0. Cancel any pending rate-limit retry timers for this story (bug 514).
|
||||
// Must happen before stopping agents so the tick loop cannot re-spawn
|
||||
// an agent after we tear everything else down.
|
||||
let timer_removed = ctx.timer_store.remove(story_id);
|
||||
if timer_removed {
|
||||
slog_warn!("[delete_story] Cancelled pending timer for '{story_id}'");
|
||||
} else {
|
||||
slog_warn!("[delete_story] No pending timer found for '{story_id}'");
|
||||
}
|
||||
let outcome = crate::service::work_item::delete::delete_work_item(
|
||||
story_id,
|
||||
&project_root,
|
||||
&ctx.services.agents,
|
||||
Some(&ctx.timer_store),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 1. Stop any running agents for this story (best-effort).
|
||||
if let Ok(agents) = ctx.services.agents.list_agents() {
|
||||
for agent in agents.iter().filter(|a| a.story_id == story_id) {
|
||||
match ctx
|
||||
.services
|
||||
.agents
|
||||
.stop_agent(&project_root, story_id, &agent.agent_name)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
slog_warn!(
|
||||
"[delete_story] Stopped agent '{}' for '{story_id}'",
|
||||
agent.agent_name
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
slog_warn!(
|
||||
"[delete_story] Failed to stop agent '{}' for '{story_id}': {e}",
|
||||
agent.agent_name
|
||||
);
|
||||
failed_steps.push(format!("stop_agent({}): {e}", agent.agent_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove agent pool entries.
|
||||
let removed_count = ctx.services.agents.remove_agents_for_story(story_id);
|
||||
slog_warn!("[delete_story] Removed {removed_count} agent pool entries for '{story_id}'");
|
||||
|
||||
// 3. Remove worktree (best-effort).
|
||||
if let Ok(config) = crate::config::ProjectConfig::load(&project_root) {
|
||||
match crate::worktree::remove_worktree_by_story_id(&project_root, story_id, &config).await {
|
||||
Ok(()) => slog_warn!("[delete_story] Removed worktree for '{story_id}'"),
|
||||
Err(e) => slog_warn!("[delete_story] Worktree removal for '{story_id}': {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Write a CRDT tombstone op so the story is evicted from the in-memory
|
||||
// state machine and the deletion is persisted to crdt_ops (survives
|
||||
// restart). Best-effort: legacy filesystem-only stories may not have a
|
||||
// CRDT entry, so a "not found" error is expected and non-fatal.
|
||||
match crate::crdt_state::evict_item(story_id) {
|
||||
Ok(()) => {
|
||||
slog_warn!(
|
||||
"[delete_story] Evicted '{story_id}' from CRDT (tombstone persisted to crdt_ops)"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
slog_warn!("[delete_story] CRDT eviction for '{story_id}': {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Delete from database content store and shadow table.
|
||||
let found_in_db = crate::db::read_content(story_id).is_some()
|
||||
|| crate::pipeline_state::read_typed(story_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
crate::db::delete_item(story_id);
|
||||
slog_warn!("[delete_story] Deleted '{story_id}' from content store / shadow table");
|
||||
|
||||
// 6. Remove the filesystem shadow file from work/N_stage/.
|
||||
let sk = project_root.join(".huskies").join("work");
|
||||
let stage_dirs = [
|
||||
"1_backlog",
|
||||
"2_current",
|
||||
"3_qa",
|
||||
"4_merge",
|
||||
"5_done",
|
||||
"6_archived",
|
||||
];
|
||||
let mut deleted_from_fs = false;
|
||||
for stage in &stage_dirs {
|
||||
let path = sk.join(stage).join(format!("{story_id}.md"));
|
||||
if path.exists() {
|
||||
match fs::remove_file(&path) {
|
||||
Ok(()) => {
|
||||
slog_warn!(
|
||||
"[delete_story] Deleted filesystem shadow '{story_id}' from work/{stage}/"
|
||||
);
|
||||
deleted_from_fs = true;
|
||||
}
|
||||
Err(e) => {
|
||||
slog_warn!(
|
||||
"[delete_story] Failed to delete filesystem shadow '{story_id}' from work/{stage}/: {e}"
|
||||
);
|
||||
failed_steps.push(format!("delete_filesystem({stage}): {e}"));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_in_db && !deleted_from_fs && !timer_removed {
|
||||
return Err(format!(
|
||||
"Story '{story_id}' not found in any pipeline stage."
|
||||
));
|
||||
}
|
||||
|
||||
if !failed_steps.is_empty() {
|
||||
if !outcome.failed_steps.is_empty() {
|
||||
return Err(format!(
|
||||
"Story '{story_id}' partially deleted. Failed steps: {}.",
|
||||
failed_steps.join("; ")
|
||||
outcome.failed_steps.join("; ")
|
||||
));
|
||||
}
|
||||
|
||||
@@ -166,6 +61,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::http::test_helpers::test_ctx;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
|
||||
fn setup_git_repo_in(dir: &std::path::Path) {
|
||||
std::process::Command::new("git")
|
||||
|
||||
Reference in New Issue
Block a user