feat: add unblock command and MCP tool to reset blocked stories
- Add `unblock` bot command (chat + web UI slash command) that clears the `blocked` flag and resets `retry_count` to 0 in story front matter - Works across all pipeline stages (1_backlog through 6_archived) - Returns confirmation with story name and ID, or clear error if story is not found or not blocked - Expose `unblock_story` MCP tool for programmatic use by agents - Make `chat::commands::unblock` module pub(crate) so story_tools can call `unblock_by_number` - Add 8 unit tests covering registration, validation, core logic, and edge cases (not-found, not-blocked, any stage, story ID in response) - Update MCP tools list test: 49 → 50 tools
This commit is contained in:
@@ -1006,6 +1006,20 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
"required": ["story_id", "target_stage"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unblock_story",
|
||||
"description": "Clear the blocked flag and reset retry_count to 0 on a work item. Use this when an agent is stuck and needs to be restarted from a clean state.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"story_id": {
|
||||
"type": "string",
|
||||
"description": "Work item identifier (filename stem, e.g. '42_story_my_feature')"
|
||||
}
|
||||
},
|
||||
"required": ["story_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Execute a shell command in an agent's worktree directory. The working_dir must be inside .storkit/worktrees/. Returns stdout, stderr, exit_code, and timed_out. Supports SSE streaming (send Accept: text/event-stream) for long-running commands. Dangerous commands (rm -rf /, sudo, etc.) are blocked.",
|
||||
@@ -1230,6 +1244,8 @@ async fn handle_tools_call(
|
||||
"delete_story" => story_tools::tool_delete_story(&args, ctx).await,
|
||||
// Arbitrary pipeline movement
|
||||
"move_story" => diagnostics::tool_move_story(&args, ctx),
|
||||
// Unblock story
|
||||
"unblock_story" => story_tools::tool_unblock_story(&args, ctx),
|
||||
// Shell command execution
|
||||
"run_command" => shell_tools::tool_run_command(&args, ctx).await,
|
||||
// Git operations
|
||||
@@ -1350,6 +1366,7 @@ mod tests {
|
||||
assert!(names.contains(&"rebuild_and_restart"));
|
||||
assert!(names.contains(&"get_token_usage"));
|
||||
assert!(names.contains(&"move_story"));
|
||||
assert!(names.contains(&"unblock_story"));
|
||||
assert!(names.contains(&"delete_story"));
|
||||
assert!(names.contains(&"run_command"));
|
||||
assert!(names.contains(&"git_status"));
|
||||
@@ -1359,7 +1376,7 @@ mod tests {
|
||||
assert!(names.contains(&"git_log"));
|
||||
assert!(names.contains(&"status"));
|
||||
assert!(names.contains(&"loc_file"));
|
||||
assert_eq!(tools.len(), 50);
|
||||
assert_eq!(tools.len(), 51);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -402,6 +402,24 @@ pub(super) fn tool_close_bug(args: &Value, ctx: &AppContext) -> Result<String, S
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn tool_unblock_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 root = ctx.state.get_project_root()?;
|
||||
|
||||
// Extract the numeric prefix (e.g. "42" from "42_story_foo")
|
||||
let story_number = story_id
|
||||
.split('_')
|
||||
.next()
|
||||
.filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
|
||||
.ok_or_else(|| format!("Invalid story_id format: '{story_id}'. Expected a numeric prefix (e.g. '42_story_foo')."))?;
|
||||
|
||||
Ok(crate::chat::commands::unblock::unblock_by_number(&root, story_number))
|
||||
}
|
||||
|
||||
pub(super) async fn tool_delete_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
let story_id = args
|
||||
.get("story_id")
|
||||
|
||||
Reference in New Issue
Block a user