diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs index abe2e3f..c5494cf 100644 --- a/server/src/handlers/meta.rs +++ b/server/src/handlers/meta.rs @@ -383,18 +383,14 @@ impl McpTool for DecisionsHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let ns = req - .namespace - .unwrap_or_else(crate::models::default_namespace); - match req.action { - DecisionAction::Log => { - let title = req.title.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'title' for action 'log'. Next step: Provide ADR 'title' string in request and retry.".to_string()) - })?; - let status = req.status.unwrap_or_else(|| "accepted".to_string()); - let context = req.context.unwrap_or_default(); - let decision = req.decision.unwrap_or_default(); - let consequence = req.consequences.unwrap_or_default(); + match req { + DecisionsTool::Log { title, status, context, decision, consequences, namespace, repo_name, author, affected_components, alternatives_considered, supersedes, git_commit, git_branch, task_id } => { + let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace); + let title = title; + let status = status.unwrap_or_else(|| "accepted".to_string()); + let context = context.unwrap_or_default(); + let decision = decision.unwrap_or_default(); + let consequence = consequences.unwrap_or_default(); let status_lower = status.to_ascii_lowercase(); let resolved_at = if status_lower == "implemented" || status_lower == "resolved" { Some(crate::handlers::utils::now_secs()) @@ -407,7 +403,7 @@ impl McpTool for DecisionsHandler { let mut adrs_to_index = Vec::new(); state.code.adrs.modify(|adrs| { - if let Some(superseded_id) = &req.supersedes { + if let Some(superseded_id) = &supersedes { for old_adr in adrs.iter_mut() { if old_adr.id.eq_ignore_ascii_case(superseded_id) { old_adr.status = "superseded".to_string(); @@ -425,17 +421,17 @@ impl McpTool for DecisionsHandler { decision: decision.clone(), consequence, status, - supersedes: req.supersedes, + supersedes: supersedes, timestamp: crate::handlers::utils::now_secs(), namespace: ns, - repo_name: req.repo_name, - alternatives_considered: req.alternatives_considered.unwrap_or_default(), - affected_components: req.affected_components.unwrap_or_default(), - author: req.author, - git_commit: req.git_commit, - git_branch: req.git_branch, + repo_name: repo_name, + alternatives_considered: alternatives_considered.unwrap_or_default(), + affected_components: affected_components.unwrap_or_default(), + author: author, + git_commit: git_commit, + git_branch: git_branch, resolved_at, - task_id: req.task_id, + task_id: task_id, }; adrs_to_index.push(a.clone()); @@ -453,17 +449,15 @@ impl McpTool for DecisionsHandler { ); Ok(format!("Logged decision {}: {}", final_id, title)) } - DecisionAction::Update => { - let id = req.id.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide ADR 'id' string in request and retry.".to_string()) - })?; + DecisionsTool::Update { id, title, status, context, decision, consequences, git_commit, git_branch, task_id, namespace: _, repo_name, author, affected_components, alternatives_considered, supersedes } => { + let id = id; let mut updated_adr = None; let mut adrs_to_index = Vec::new(); state.code.adrs.modify(|adrs| { let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id)); if let Some(pos) = target_pos { - if let Some(superseded_id) = &req.supersedes + if let Some(superseded_id) = &supersedes && let Some(s_pos) = adrs .iter() .position(|a| a.id.eq_ignore_ascii_case(superseded_id)) @@ -474,19 +468,19 @@ impl McpTool for DecisionsHandler { } let a = &mut adrs[pos]; - if let Some(t) = req.title { + if let Some(t) = title { a.title = t; } - if let Some(c) = req.context { + if let Some(c) = context { a.context = c; } - if let Some(d) = req.decision { + if let Some(d) = decision { a.decision = d; } - if let Some(cons) = req.consequences { + if let Some(cons) = consequences { a.consequence = cons; } - if let Some(s) = req.status { + if let Some(s) = status { let s_lower = s.to_ascii_lowercase(); if (s_lower == "implemented" || s_lower == "resolved") && a.resolved_at.is_none() @@ -497,29 +491,29 @@ impl McpTool for DecisionsHandler { } a.status = s; } - if req.supersedes.is_some() { - a.supersedes = req.supersedes; + if supersedes.is_some() { + a.supersedes = supersedes; } - if req.repo_name.is_some() { - a.repo_name = req.repo_name; + if repo_name.is_some() { + a.repo_name = repo_name; } - if let Some(alts) = req.alternatives_considered { + if let Some(alts) = alternatives_considered { a.alternatives_considered = alts; } - if let Some(aff) = req.affected_components { + if let Some(aff) = affected_components { a.affected_components = aff; } - if req.author.is_some() { - a.author = req.author; + if author.is_some() { + a.author = author; } - if req.git_commit.is_some() { - a.git_commit = req.git_commit; + if git_commit.is_some() { + a.git_commit = git_commit; } - if req.git_branch.is_some() { - a.git_branch = req.git_branch; + if git_branch.is_some() { + a.git_branch = git_branch; } - if req.task_id.is_some() { - a.task_id = req.task_id; + if task_id.is_some() { + a.task_id = task_id; } adrs_to_index.push(a.clone()); @@ -548,9 +542,10 @@ impl McpTool for DecisionsHandler { ))) } } - DecisionAction::Query => { - let limit = req.limit.unwrap_or(20); - let include_body = req.include_body.unwrap_or(true); + DecisionsTool::Query { query, namespace, limit, include_body } => { + let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace); + let limit = limit.unwrap_or(20); + let include_body = include_body.unwrap_or(true); let data = state.code.adrs.read_with(|adrs| { let filtered: Vec<_> = adrs .iter() @@ -558,7 +553,7 @@ impl McpTool for DecisionsHandler { if !a.namespace.eq_ignore_ascii_case(&ns) && ns != "global" { return false; } - if let Some(q) = &req.query { + if let Some(q) = &query { crate::handlers::utils::contains_ignore_ascii_case(&a.title, q) || crate::handlers::utils::contains_ignore_ascii_case( &a.context, q, @@ -601,10 +596,8 @@ impl McpTool for DecisionsHandler { })?; Ok(data) } - DecisionAction::Delete => { - let id = req.id.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'id' for action 'delete'. Next step: Provide ADR 'id' string in request and retry.".to_string()) - })?; + DecisionsTool::Delete { id } => { + let id = id; let mut found = false; state.code.adrs.modify(|adrs| { if let Some(pos) = adrs.iter().position(|a| a.id == id) { @@ -644,15 +637,11 @@ impl McpTool for TechDebtHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let ns = req - .namespace - .unwrap_or_else(crate::models::default_namespace); - match req.action { - TechDebtAction::Log => { - let desc = req.description.or(req.title).ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'description' for action 'log'. Next step: Provide tech debt 'description' in request and retry.".to_string()) - })?; - let ideal = req.ideal_solution.unwrap_or_default(); + match req { + TechDebtTool::Log { description, title, ideal_solution, git_commit, git_branch, symbol_references, line_range, namespace, severity, repo_name, file_path, workaround, effort_estimate } => { + let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace); + let desc = description; + let ideal = ideal_solution.unwrap_or_default(); let text_to_embed = format!("Description: {}\nIdeal Solution: {}", desc, ideal); let embedding = crate::embedding::generate_embedding_async(text_to_embed) .await @@ -665,15 +654,15 @@ impl McpTool for TechDebtHandler { ideal_solution: ideal, is_resolved: false, created_at: crate::handlers::utils::now_secs(), - git_commit: req.git_commit, - git_branch: req.git_branch, + git_commit: git_commit, + git_branch: git_branch, embedding, - repo_name: req.repo_name, - severity: req.severity, - file_path: req.file_path, - line_range: req.line_range, - workaround: req.workaround, - effort_estimate: req.effort_estimate, + repo_name: repo_name, + severity: severity, + file_path: file_path, + line_range: line_range, + workaround: workaround, + effort_estimate: effort_estimate, }); if debts.len() > 300 { let severity_rank = @@ -697,10 +686,8 @@ impl McpTool for TechDebtHandler { }); Ok("Tech debt logged".to_string()) } - TechDebtAction::Resolve => { - let id = req.id.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'id' for action 'resolve'. Next step: Provide tech debt 'id' string in request and retry.".to_string()) - })?; + TechDebtTool::Resolve { id } => { + let id = id; let mut found = false; state.code.tech_debts.modify(|debts| { for d in debts.iter_mut() { @@ -720,9 +707,10 @@ impl McpTool for TechDebtHandler { )) } } - TechDebtAction::List => { - let inc = req.include_resolved.unwrap_or(false); - let level = req.summary_level.as_deref().unwrap_or("detailed"); + TechDebtTool::List { include_resolved, namespace, summary_level, max_tokens } => { + let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace); + let inc = include_resolved.unwrap_or(false); + let level = summary_level.as_deref().unwrap_or("detailed"); let data = state.code.tech_debts.read_with(|debts| { let filtered: Vec<_> = debts .iter() @@ -743,7 +731,7 @@ impl McpTool for TechDebtHandler { }) .collect(); let mut json_str = serde_yaml::to_string(&filtered)?; - if let Some(max_t) = req.max_tokens { + if let Some(max_t) = max_tokens { let char_limit = max_t * 4; if json_str.len() > char_limit { json_str.truncate(char_limit); @@ -1199,13 +1187,9 @@ impl McpTool for ManageCheckpointHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: ManageCheckpointTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - match req.action { - CheckpointAction::Create => { - let name = req.name_or_id.ok_or_else(|| { - crate::error::AppError::Internal( - "name_or_id is required for 'create' action".to_string(), - ) - })?; + match req { + ManageCheckpointTool::Create { name_or_id, description, namespace } => { + let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?; let target_dir = state.base_dir.join("checkpoints").join(&name); if let Err(e) = std::fs::create_dir_all(&target_dir) { @@ -1230,15 +1214,12 @@ impl McpTool for ManageCheckpointHandler { .read_with(|d| serde_json::to_string(d).unwrap_or_default()); let _ = std::fs::write(target_dir.join("tech_debts.json"), debts_json); - if let Some(desc) = &req.description { + if let Some(desc) = &description { let snapshot_id = format!( "SNAP-{}", uuid::Uuid::new_v4().to_string()[..8].to_uppercase() ); - let ns = req - .namespace - .clone() - .unwrap_or_else(|| "global".to_string()); + let ns = namespace.clone().unwrap_or_else(|| "global".to_string()); let snapshot = crate::models::StateSnapshot { id: snapshot_id, timestamp: crate::handlers::utils::now_secs(), @@ -1251,12 +1232,8 @@ impl McpTool for ManageCheckpointHandler { Ok(format!("Checkpoint '{}' created successfully.", name)) } - CheckpointAction::Restore => { - let name = req.name_or_id.ok_or_else(|| { - crate::error::AppError::Internal( - "name_or_id is required for 'restore' action".to_string(), - ) - })?; + ManageCheckpointTool::Restore { name_or_id, namespace } => { + let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?; let target_dir = state.base_dir.join("checkpoints").join(&name); if !target_dir.exists() { @@ -1297,7 +1274,7 @@ impl McpTool for ManageCheckpointHandler { Ok(format!("Checkpoint '{}' restored successfully.", name)) } - CheckpointAction::List => { + ManageCheckpointTool::List { namespace } => { let mut list = Vec::new(); let checkpoints_dir = state.base_dir.join("checkpoints"); if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) { @@ -1315,12 +1292,8 @@ impl McpTool for ManageCheckpointHandler { } Ok(serde_yaml::to_string(&list)?) } - CheckpointAction::Delete => { - let name = req.name_or_id.ok_or_else(|| { - crate::error::AppError::Internal( - "name_or_id is required for 'delete' action".to_string(), - ) - })?; + ManageCheckpointTool::Delete { name_or_id, namespace } => { + let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?; let target_dir = state.base_dir.join("checkpoints").join(&name); if target_dir.exists() { let _ = std::fs::remove_dir_all(&target_dir); @@ -1516,14 +1489,9 @@ impl McpTool for HypothesesHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: HypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - - match req.action { - HypothesisAction::Log => { - let hyp_text = req.hypothesis.ok_or_else(|| { - crate::error::AppError::Internal( - "Missing required 'hypothesis' for action 'log'".to_string(), - ) - })?; + match req { + HypothesesTool::Log { hypothesis, task_id, status, evidence } => { + let hyp_text = hypothesis; let hyp_id = format!( "HYP-{}", uuid::Uuid::new_v4().to_string()[..8].to_uppercase() @@ -1532,10 +1500,10 @@ impl McpTool for HypothesesHandler { let record = crate::models::Hypothesis { id: hyp_id.clone(), - task_id: req.task_id, + task_id: task_id, hypothesis: hyp_text, - status: req.status.unwrap_or_else(|| "unverified".to_string()), - evidence: req.evidence, + status: status.unwrap_or_else(|| "unverified".to_string()), + evidence: evidence, timestamp, ..Default::default() }; @@ -1544,17 +1512,17 @@ impl McpTool for HypothesesHandler { Ok(format!("Hypothesis '{}' logged successfully.", hyp_id)) } - HypothesisAction::Query => { + HypothesesTool::Query { query, task_id, status } => { let hypotheses = state.code.hypotheses.read_with(|h| h.clone()); let filtered: Vec<_> = hypotheses .into_iter() .filter(|h| { - if let Some(tid) = &req.task_id + if let Some(tid) = &task_id && h.task_id.as_ref() != Some(tid) { return false; } - if let Some(q) = &req.query { + if let Some(q) = &query { let lq = q.to_lowercase(); return h.hypothesis.to_lowercase().contains(&lq) || h.evidence @@ -1665,24 +1633,11 @@ impl McpTool for AgentSignalsHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - - match req.action { - AgentSignalAction::Broadcast => { - let sender = req.sender.ok_or_else(|| { - crate::error::AppError::Internal( - "Missing required 'sender' for action 'broadcast'".to_string(), - ) - })?; - let signal_type = req.signal_type.ok_or_else(|| { - crate::error::AppError::Internal( - "Missing required 'signal_type' for action 'broadcast'".to_string(), - ) - })?; - let payload = req.payload.ok_or_else(|| { - crate::error::AppError::Internal( - "Missing required 'payload' for action 'broadcast'".to_string(), - ) - })?; + match req { + AgentSignalsTool::Broadcast { sender, signal_type, payload, ttl_seconds } => { + let sender = sender; + let signal_type = signal_type; + let payload = payload; let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -1696,7 +1651,7 @@ impl McpTool for AgentSignalsHandler { signal_type: signal_type.clone(), payload, timestamp, - ttl_seconds: req.ttl_seconds, + ttl_seconds: ttl_seconds, ..Default::default() }; @@ -1724,7 +1679,7 @@ impl McpTool for AgentSignalsHandler { sig_id, sender )) } - AgentSignalAction::Query => { + AgentSignalsTool::Query { sender, signal_type, limit } => { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() @@ -1739,19 +1694,19 @@ impl McpTool for AgentSignalsHandler { { return false; } - if let Some(sender) = &req.sender + if let Some(sender) = &sender && s.sender.to_lowercase() != sender.to_lowercase() { return false; } - if let Some(st) = &req.signal_type + if let Some(st) = &signal_type && s.signal_type.to_lowercase() != st.to_lowercase() { return false; } true }) - .take(req.limit.unwrap_or(20)) + .take(limit.unwrap_or(20)) .cloned() .collect::>() }); diff --git a/server/src/handlers/tasks.rs b/server/src/handlers/tasks.rs index 167af4e..b786b1a 100644 --- a/server/src/handlers/tasks.rs +++ b/server/src/handlers/tasks.rs @@ -23,15 +23,12 @@ impl McpTool for TasksHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - match req.action { - TaskAction::Add => { - let title = req.title.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string()) - })?; - let description = req.description.unwrap_or_default(); + match req { + TasksTool::Add { title, description, parent_id, dependencies, git_branch, namespace, repo_name, priority, assigned_agent, verification_command } => { + let description = description.unwrap_or_default(); let now = crate::handlers::utils::now_secs(); let task_id = uuid::Uuid::new_v4().to_string(); - let deps = req.dependencies.unwrap_or_default(); + let deps = dependencies.unwrap_or_default(); let task = Task { id: task_id.clone(), @@ -40,16 +37,16 @@ impl McpTool for TasksHandler { description, created_at: now, updated_at: now, - git_branch: req.git_branch, - parent_id: req.parent_id, + git_branch, + parent_id, dependencies: deps, acceptance_criteria: vec![], expires_at: None, - namespace: req.namespace.unwrap_or_else(|| "default".to_string()), - repo_name: req.repo_name, - priority: req.priority, - assigned_agent: req.assigned_agent, - verification_command: req.verification_command, + namespace: namespace.unwrap_or_else(|| "default".to_string()), + repo_name, + priority, + assigned_agent, + verification_command, }; let idx = state.get_search_index().await; drop(idx.index_task(&task)); @@ -75,13 +72,7 @@ impl McpTool for TasksHandler { }); Ok(format!("Task added with ID: {}", task_id)) } - TaskAction::Update => { - let id = req.id.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide task 'id' string in request and retry.".to_string()) - })?; - let status = req.status.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'active', 'in_progress', 'blocked', 'completed', or 'cancelled') in request and retry.".to_string()) - })?; + TasksTool::Update { id, status } => { let target_status = status.to_lowercase(); let mut found = false; let mut blocked = false; @@ -202,10 +193,7 @@ impl McpTool for TasksHandler { )) } } - TaskAction::Delete => { - let id = req.id.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'id' for action 'delete'. Next step: Provide task 'id' string in request and retry.".to_string()) - })?; + TasksTool::Delete { id } => { let mut deleted_count = 0; let mut actually_deleted = Vec::new(); state.project.tasks.modify(|tasks| { @@ -267,19 +255,19 @@ impl McpTool for TasksHandler { )) } } - TaskAction::List => { - let level = req.summary_level.as_deref().unwrap_or("detailed"); - let offset = req.offset.unwrap_or(0); + TasksTool::List { git_branch, status, summary_level, max_tokens, offset } => { + let level = summary_level.as_deref().unwrap_or("detailed"); + let offset = offset.unwrap_or(0); let data = state.project.tasks.read_with(|tasks| { let matching: Vec<_> = tasks .iter() .filter(|t| { - let status_match = match req.status.as_deref() { + let status_match = match status.as_deref() { Some("all") => true, Some(s) => t.status.eq_ignore_ascii_case(s), None => t.is_active(), }; - let branch_match = match &req.git_branch { + let branch_match = match &git_branch { Some(branch) => t.git_branch.is_none() || t.git_branch.as_deref() == Some(branch.as_str()), None => true, }; @@ -297,7 +285,7 @@ impl McpTool for TasksHandler { }) .collect(); let mut json_str = serde_yaml::to_string(&filtered)?; - if let Some(max_t) = req.max_tokens { + if let Some(max_t) = max_tokens { let char_limit = max_t * 4; while json_str.len() > char_limit && !filtered.is_empty() { filtered.pop(); @@ -308,13 +296,7 @@ impl McpTool for TasksHandler { })?; Ok(data) } - TaskAction::SetCriteria => { - let id = req.id.or(req.title.clone()).ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'id' or 'title' for action 'set_criteria'. Next step: Provide task 'id' string in request and retry.".to_string()) - })?; - let criteria_list = req.criteria.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'criteria' for action 'set_criteria'. Next step: Provide array of acceptance criteria strings in request and retry.".to_string()) - })?; + TasksTool::SetCriteria { id, criteria: criteria_list } => { let mut success = false; let mut updated_task: Option = None; state.project.tasks.modify(|tasks| { @@ -355,11 +337,8 @@ impl McpTool for TasksHandler { )) } } - TaskAction::Verify => { - let id = req.id.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'id' for action 'verify'. Next step: Provide task 'id' string in request and retry.".to_string()) - })?; - let proof_str = req.proof.unwrap_or_else(|| "Verified".to_string()); + TasksTool::Verify { id, proof } => { + let proof_str = proof.unwrap_or_else(|| "Verified".to_string()); let mut success = false; let mut already_met = false; let mut updated_task: Option = None; @@ -436,41 +415,29 @@ impl McpTool for MilestonesHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - match req.action { - MilestoneAction::Add => { - let title = req.title.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string()) - })?; - let ns = req - .namespace - .unwrap_or_else(crate::models::default_namespace); + match req { + MilestonesTool::Add { title, namespace, target_date, description, deliverables, task_ids, repo_name } => { + let ns = namespace.unwrap_or_else(crate::models::default_namespace); state.project.milestones.modify(|ms| { ms.push(crate::models::Milestone { id: uuid::Uuid::new_v4().to_string(), title, status: "pending".to_string(), namespace: ns, - target_date: req - .target_date + target_date: target_date .as_deref() .and_then(|s| s.parse::().ok()), - description: req.description, - task_ids: req.task_ids.unwrap_or_default(), - deliverables: req.deliverables.unwrap_or_default(), - repo_name: req.repo_name, + description: description, + task_ids: task_ids.unwrap_or_default(), + deliverables: deliverables.unwrap_or_default(), + repo_name, ..Default::default() }) }); crate::handlers::reconciliation::recalculate_milestones(&state); Ok("Milestone added".to_string()) } - MilestoneAction::Update => { - let id = req.id.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide milestone 'id' string in request and retry.".to_string()) - })?; - let status = req.status.ok_or_else(|| { - crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide milestone 'status' in request and retry.".to_string()) - })?; + MilestonesTool::Update { id, status, deliverables, task_ids } => { let mut found = false; state.project.milestones.modify(|ms| { for m in ms.iter_mut() { @@ -482,9 +449,9 @@ impl McpTool for MilestonesHandler { { m.completed_at = Some(crate::handlers::utils::now_secs()); } - if let Some(task_ids) = &req.task_ids { + if let Some(task_ids) = &task_ids { m.task_ids = task_ids.clone(); - if let Some(deliverables) = &req.deliverables { + if let Some(deliverables) = &deliverables { m.deliverables = deliverables.clone(); } } @@ -502,12 +469,12 @@ impl McpTool for MilestonesHandler { )) } } - MilestoneAction::List => { + MilestonesTool::List { namespace } => { let data = state.project.milestones.read_with(|items| { let filtered: Vec<_> = items .iter() .filter(|i| { - if let Some(ns) = &req.namespace { + if let Some(ns) = &namespace { &i.namespace == ns } else { true diff --git a/server/src/tools.rs b/server/src/tools.rs index f6cba5b..e451f45 100644 --- a/server/src/tools.rs +++ b/server/src/tools.rs @@ -186,26 +186,38 @@ pub struct GetSubgraphTool { pub max_tokens: Option, } -#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum CheckpointAction { - Create, - Restore, - List, - Delete, -} - /// Save, restore, list, or delete point-in-time memory state snapshot checkpoints. #[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct ManageCheckpointTool { - /// Action to perform: 'create', 'restore', 'list', or 'delete'. - pub action: CheckpointAction, - /// Unique name or ID for the snapshot checkpoint. - pub name_or_id: Option, - /// Optional description of why this checkpoint was created. - pub description: Option, - /// Optional namespace filter. - pub namespace: Option, +#[serde(tag = "action", rename_all = "snake_case")] +pub enum ManageCheckpointTool { + /// Save a checkpoint. + Create { + /// Unique name or ID for the snapshot checkpoint. + name_or_id: Option, + /// Optional description of why this checkpoint was created. + description: Option, + /// Optional namespace filter. + namespace: Option, + }, + /// Restore a checkpoint. + Restore { + /// Unique name or ID for the snapshot checkpoint. + name_or_id: Option, + /// Optional namespace filter. + namespace: Option, + }, + /// List checkpoints. + List { + /// Optional namespace filter. + namespace: Option, + }, + /// Delete a checkpoint. + Delete { + /// Unique name or ID for the snapshot checkpoint. + name_or_id: Option, + /// Optional namespace filter. + namespace: Option, + } } /// Merge two entities in the knowledge graph into one. @@ -360,28 +372,30 @@ pub struct GetNextActionableTasksTool { pub limit: Option, } -#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum HypothesisAction { - Log, - Query, -} - /// Manage diagnostic hypotheses, tested evidence, and status during problem solving. #[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct HypothesesTool { - /// Action to perform: 'log' or 'query'. - pub action: HypothesisAction, - /// Optional task ID associated with this hypothesis or to filter hypotheses. - pub task_id: Option, - /// The diagnostic hypothesis or potential root cause (required for action 'log'). - pub hypothesis: Option, - /// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'. - pub status: Option, - /// Evidence or test results supporting or disproving the hypothesis. - pub evidence: Option, - /// Optional search query text (for action 'query'). - pub query: Option, +#[serde(tag = "action", rename_all = "snake_case")] +pub enum HypothesesTool { + /// Log a new diagnostic hypothesis. + Log { + /// The diagnostic hypothesis or potential root cause. + hypothesis: String, + /// Optional task ID associated with this hypothesis. + task_id: Option, + /// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'. + status: Option, + /// Evidence or test results supporting or disproving the hypothesis. + evidence: Option, + }, + /// Query existing hypotheses. + Query { + /// Optional search query text. + query: Option, + /// Optional task ID to filter hypotheses. + task_id: Option, + /// Optional status filter. + status: Option, + } } #[cfg(test)] mod tests { @@ -577,28 +591,30 @@ pub struct SummarizeSubgraphTool { pub max_tokens: Option, } -#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum AgentSignalAction { - Broadcast, - Query, -} - /// Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents. #[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct AgentSignalsTool { - /// Action to perform: 'broadcast' or 'query'. - pub action: AgentSignalAction, - /// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). Required for broadcast; optional filter for query. - pub sender: Option, - /// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). Required for broadcast; optional filter for query. - pub signal_type: Option, - /// JSON or text payload containing event details or artifact URIs (required for action 'broadcast'). - pub payload: Option, - /// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour). - pub ttl_seconds: Option, - /// Optional limit on returned signals (for action 'query'). Defaults to 20. - pub limit: Option, +#[serde(tag = "action", rename_all = "snake_case")] +pub enum AgentSignalsTool { + /// Broadcast a signal to peer subagents. + Broadcast { + /// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). + sender: String, + /// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). + signal_type: String, + /// JSON or text payload containing event details or artifact URIs. + payload: String, + /// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour). + ttl_seconds: Option, + }, + /// Query active signals. + Query { + /// Optional sender agent ID or role filter. + sender: Option, + /// Optional signal type filter. + signal_type: Option, + /// Optional limit on returned signals. Defaults to 20. + limit: Option, + } } /// Trigger an automated context checkpoint, summarizing active tasks, hypotheses, recent commits, and open tech debt into a permanent HandoffMemo. #[derive(Debug, Deserialize, Serialize, JsonSchema)] @@ -620,12 +636,6 @@ pub enum HandoffMemoAction { } /// Persistent handoff memos for passing session context to future agents. -/// Actions: -/// - 'leave': Post a handoff memo. Required: content. Optional: namespace. -/// - 'read': Inspect active memos. Optional: namespace. -/// - 'clear': Clear memos. Optional: namespace, ids. -/// -/// Next steps on error: Check parameter requirements for 'leave' or 'clear' actions. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct HandoffMemosTool { /// Action to perform: 'leave', 'read', or 'clear'. @@ -638,105 +648,113 @@ pub struct HandoffMemosTool { pub ids: Option>, } -#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum TaskAction { - Add, - Update, - Delete, - List, - SetCriteria, - Verify, -} - -/// Action-oriented task tracking system (add, update, delete, list, set_criteria, verify). -/// Actions: -/// - 'add': Create task. Required: title. Optional: description, git_branch, parent_id, dependencies. -/// - 'update': Update task status. Required: id, status ('pending'|'completed'|'cancelled'). -/// - 'delete': Delete task & subtasks. Required: id. -/// - 'list': List active tasks. Optional: git_branch, summary_level ('compact'|'detailed'|'full'), max_tokens. -/// - 'set_criteria': Set acceptance criteria. Required: id, criteria (array of strings). -/// - 'verify': Verify criteria met. Required: id. -/// -/// Next steps on error: Check required parameters or call list to verify task IDs. +/// Action-oriented task tracking system. #[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct TasksTool { - /// Action to perform: 'add', 'update', 'delete', 'list', 'set_criteria', or 'verify'. - pub action: TaskAction, - /// Task ID (required for 'update', 'delete', 'set_criteria', 'verify'). - pub id: Option, - /// Task title (required for 'add'). - pub title: Option, - /// Task description (optional for 'add'). - pub description: Option, - /// New status: 'pending', 'completed', or 'cancelled' (for 'update'). - pub status: Option, - /// Parent task ID (optional for 'add'). - pub parent_id: Option, - /// List of dependency task IDs (optional for 'add'). - pub dependencies: Option>, - /// Git branch filter or assignment. - pub git_branch: Option, - /// Acceptance criteria (required for 'set_criteria'). - pub criteria: Option>, - /// Verification proof or details (optional for 'verify'). - pub proof: Option, - /// Summary level: 'compact', 'detailed', or 'full' (for 'list'). - pub summary_level: Option, - /// Maximum tokens budget cap (for 'list'). - pub max_tokens: Option, - /// Optional namespace filter or assignment (defaults to 'default'). - pub namespace: Option, - /// Optional repository name. - pub repo_name: Option, - /// Optional task priority: 'low', 'medium', 'high', 'urgent'. - pub priority: Option, - /// Optional assigned subagent role or identifier. - pub assigned_agent: Option, - /// Optional verification command to validate criteria completion. - pub verification_command: Option, - /// Optional pagination offset for 'list'. - pub offset: Option, +#[serde(tag = "action", rename_all = "snake_case")] +pub enum TasksTool { + /// Create a new task. + Add { + /// Task title. + title: String, + /// Task description. + description: Option, + /// Optional parent task ID for subtasks. + parent_id: Option, + /// List of dependent task IDs. + dependencies: Option>, + /// Branch associated with this task. + git_branch: Option, + /// Optional namespace filter or assignment. + namespace: Option, + /// Repository name. + repo_name: Option, + /// Priority: 'low', 'medium', 'high', 'critical'. + priority: Option, + /// Assigned agent or subagent. + assigned_agent: Option, + /// Command used to verify completion. + verification_command: Option, + }, + /// Update task status. + Update { + /// Task ID. + id: String, + /// New status: 'pending', 'completed', or 'cancelled'. + status: String, + }, + /// Delete a task and its subtasks. + Delete { + /// Task ID. + id: String, + }, + /// List active tasks. + List { + /// Optional Git branch filter. + git_branch: Option, + /// Optional status filter. + status: Option, + /// Summary level: 'compact', 'detailed', or 'full'. Defaults to 'detailed'. + summary_level: Option, + /// Maximum tokens budget cap. + max_tokens: Option, + /// Optional pagination offset. + offset: Option, + }, + /// Set a checklist of acceptance criteria. + SetCriteria { + /// Task ID or title. + id: String, + /// Acceptance criteria list. + criteria: Vec, + }, + /// Mark an acceptance criteria as met with cryptographic-like proof. + Verify { + /// Task ID or title. + id: String, + /// Verification proof or details. + proof: Option, + } } -#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum MilestoneAction { - Add, - Update, - List, -} - -/// Project milestone management (add, update, list). -/// Actions: -/// - 'add': Create milestone. Required: title. Optional: namespace. -/// - 'update': Update milestone status. Required: id, status. -/// - 'list': List milestones. Optional: namespace. -/// -/// Next steps on error: Provide required title for 'add' or id/status for 'update'. +/// Project milestone management. #[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct MilestonesTool { - /// Action to perform: 'add', 'update', or 'list'. - pub action: MilestoneAction, - /// Milestone ID (required for 'update'). - pub id: Option, - /// Milestone title (required for 'add'). - pub title: Option, - /// New status (for 'update'). - pub status: Option, - /// Optional namespace filter or assignment. - pub namespace: Option, - /// Optional target completion date. - pub target_date: Option, - /// Optional description of milestone scope. - pub description: Option, - /// Optional deliverables or task checklist. - pub deliverables: Option>, - /// Optional list of task IDs associated with this milestone. - #[serde(default)] - pub task_ids: Option>, - /// Optional repository name. - pub repo_name: Option, +#[serde(tag = "action", rename_all = "snake_case")] +pub enum MilestonesTool { + /// Create a new milestone. + Add { + /// Milestone title. + title: String, + /// Optional namespace filter or assignment. + namespace: Option, + /// Optional target completion date. + target_date: Option, + /// Optional description of milestone scope. + description: Option, + /// Optional deliverables or task checklist. + deliverables: Option>, + /// Optional list of task IDs associated with this milestone. + #[serde(default)] + task_ids: Option>, + /// Optional repository name. + repo_name: Option, + }, + /// Update milestone status. + Update { + /// Milestone ID. + id: String, + /// New status. + status: String, + /// Optional deliverables or task checklist. + deliverables: Option>, + /// Optional list of task IDs associated with this milestone. + #[serde(default)] + task_ids: Option>, + }, + /// List milestones. + List { + /// Optional namespace. + namespace: Option, + } } #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] @@ -748,13 +766,6 @@ pub enum SnippetAction { } /// Syntactically preserved reusable code snippets vault (store, search, delete, tag). -/// Actions: -/// - 'store': Store snippet. Required: query (or id) as snippet name. Optional: language, code, description, tags. -/// - 'search': Search snippets. Optional: query, tags, hybrid (boolean for BM25+vector search). -/// - 'delete': Delete snippet. Required: id (or query) as snippet name. -/// - 'tag': Tag snippet. Required: id (or query), tags (array of strings). -/// -/// Next steps on error: Ensure snippet name/query or id is provided. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct SnippetsTool { /// Action to perform: 'store', 'search', 'delete', or 'tag'. @@ -785,118 +796,141 @@ pub struct SnippetsTool { pub line_range: Option, } -#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Clone)] -#[serde(rename_all = "snake_case")] -pub enum DecisionAction { - Log, - Query, - Delete, - Update, -} - -/// Architectural Decision Records (ADRs) log (log, query, update, delete). -/// Actions: -/// - 'log': Record ADR. Required: title. Optional: status, context, decision, consequences, namespace. -/// - 'query': Search ADRs. Optional: query, namespace. -/// - 'update': Update ADR status or implementation info. Required: id. Optional: status, git_commit, git_branch, task_id, context, decision, consequences, title. -/// - 'delete': Remove ADR. Required: id. -/// -/// Next steps on error: Provide title for 'log' or id for 'update'/'delete'. +/// Architectural Decision Records (ADRs) log. #[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)] -pub struct DecisionsTool { - /// Action to perform: 'log', 'query', 'update', or 'delete'. - pub action: DecisionAction, - /// ADR ID (required for 'delete'). - pub id: Option, - /// ADR title (required for 'log'). - pub title: Option, - /// ADR status (for 'log'). - pub status: Option, - /// Context & problem statement (for 'log'). - pub context: Option, - /// Decision made (for 'log'). - pub decision: Option, - /// Consequences & tradeoffs (for 'log'). - pub consequences: Option, - /// Search query string (for 'query'). - pub query: Option, - /// Optional namespace. - pub namespace: Option, - /// Optional repository name. - pub repo_name: Option, - /// Optional author or architect behind the decision. - pub author: Option, - /// Optional affected system components or crates. - pub affected_components: Option>, - /// Optional alternative designs or libraries considered. - pub alternatives_considered: Option>, - /// Optional superseded ADR ID. - pub supersedes: Option, - /// Limit the number of query results. Defaults to 20. - pub limit: Option, - /// Include the full body of the matched decisions. Defaults to true. - pub include_body: Option, - /// Optional Git commit where decision was implemented. - pub git_commit: Option, - /// Optional Git branch where decision was implemented. - pub git_branch: Option, - /// Optional task ID linked to this decision. - pub task_id: Option, +#[serde(tag = "action", rename_all = "snake_case")] +pub enum DecisionsTool { + /// Record an ADR. + Log { + /// ADR title. + title: String, + /// ADR status. + status: Option, + /// Context & problem statement. + context: Option, + /// Decision made. + decision: Option, + /// Consequences & tradeoffs. + consequences: Option, + /// Optional namespace. + namespace: Option, + /// Optional repository name. + repo_name: Option, + /// Optional author or architect behind the decision. + author: Option, + /// Optional affected system components or crates. + affected_components: Option>, + /// Optional alternative designs or libraries considered. + alternatives_considered: Option>, + /// Optional superseded ADR ID. + supersedes: Option, + /// Optional Git commit where decision was implemented. + git_commit: Option, + /// Optional Git branch where decision was implemented. + git_branch: Option, + /// Optional task ID linked to this decision. + task_id: Option, + }, + /// Search ADRs. + Query { + /// Search query string. + query: Option, + /// Optional namespace. + namespace: Option, + /// Limit the number of query results. Defaults to 20. + limit: Option, + /// Include the full body of the matched decisions. Defaults to true. + include_body: Option, + }, + /// Update ADR status or implementation info. + Update { + /// ADR ID. + id: String, + /// ADR title. + title: Option, + /// ADR status. + status: Option, + /// Context & problem statement. + context: Option, + /// Decision made. + decision: Option, + /// Consequences & tradeoffs. + consequences: Option, + /// Optional Git commit where decision was implemented. + git_commit: Option, + /// Optional Git branch where decision was implemented. + git_branch: Option, + /// Optional task ID linked to this decision. + task_id: Option, + /// Optional namespace. + namespace: Option, + /// Optional repository name. + repo_name: Option, + /// Optional author or architect behind the decision. + author: Option, + /// Optional affected system components or crates. + affected_components: Option>, + /// Optional alternative designs or libraries considered. + alternatives_considered: Option>, + /// Optional superseded ADR ID. + supersedes: Option, + }, + /// Remove an ADR. + Delete { + /// ADR ID. + id: String, + } } -#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum TechDebtAction { - Log, - Resolve, - List, -} - -/// Technical debt backlog management (log, resolve, list). -/// Actions: -/// - 'log': Record technical debt. Required: description. Optional: ideal_solution, git_commit, git_branch, symbol_references, line_range, namespace. -/// - 'resolve': Mark tech debt resolved. Required: id. -/// - 'list': List tech debt items. Optional: include_resolved, namespace. -/// -/// Next steps on error: Provide description for 'log' or valid ID for 'resolve'. +/// Technical debt backlog management. #[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct TechDebtTool { - /// Action to perform: 'log', 'resolve', or 'list'. - pub action: TechDebtAction, - /// Tech debt ID (required for 'resolve'). - pub id: Option, - /// Tech debt description (required for 'log'). - pub description: Option, - /// Optional title or summary of tech debt. - pub title: Option, - /// Ideal solution (optional for 'log'). - pub ideal_solution: Option, - /// Include resolved tech debt items (for 'list'). - pub include_resolved: Option, - /// Associated git commit hash. - pub git_commit: Option, - /// Associated git branch. - pub git_branch: Option, - /// Symbol references associated with the tech debt. - pub symbol_references: Option>, - /// Line range associated with the tech debt. - pub line_range: Option, - /// Optional namespace. - pub namespace: Option, - /// Optional debt severity: 'low', 'medium', 'high', 'critical'. - pub severity: Option, - /// Optional repository name. - pub repo_name: Option, - /// Optional file path where debt exists. - pub file_path: Option, - /// Optional workaround currently in place. - pub workaround: Option, - /// Optional effort estimate to fix. - pub effort_estimate: Option, - /// Summary level for list action: 'compact', 'detailed', or 'full'. - pub summary_level: Option, - /// Optional token budget cap for list output. - pub max_tokens: Option, +#[serde(tag = "action", rename_all = "snake_case")] +pub enum TechDebtTool { + /// Record technical debt. + Log { + /// Tech debt description. + description: String, + /// Optional title or summary of tech debt. + title: Option, + /// Ideal solution. + ideal_solution: Option, + /// Associated git commit hash. + git_commit: Option, + /// Associated git branch. + git_branch: Option, + /// Symbol references associated with the tech debt. + symbol_references: Option>, + /// Line range associated with the tech debt. + line_range: Option, + /// Optional namespace. + namespace: Option, + /// Optional debt severity: 'low', 'medium', 'high', 'critical'. + severity: Option, + /// Optional repository name. + repo_name: Option, + /// Optional file path where debt exists. + file_path: Option, + /// Optional workaround currently in place. + workaround: Option, + /// Optional effort estimate to fix. + effort_estimate: Option, + }, + /// Mark tech debt resolved. + Resolve { + /// Tech debt ID. + id: String, + }, + /// List tech debt items. + List { + /// Include resolved tech debt items. + include_resolved: Option, + /// Optional namespace. + namespace: Option, + /// Summary level for list action: 'compact', 'detailed', or 'full'. + summary_level: Option, + /// Optional token budget cap for list output. + max_tokens: Option, + } } #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] @@ -910,14 +944,6 @@ pub enum EnvAction { } /// Environment requirements, tool fingerprints & infrastructure management. -/// Actions: -/// - 'update_fingerprint': Update tool versions. Optional: tool_versions map, namespace. -/// - 'read_fingerprint': Read current environment fingerprint. Optional: namespace. -/// - 'log_requirement': Log environment variable requirement. Required: key. Optional: description, is_secret, namespace. -/// - 'register': Register remote target environment. Required: name. Optional: url, description, requires_vpn, namespace. -/// - 'get_details': Get full environment details. Optional: namespace. -/// -/// Next steps on error: Provide key for 'log_requirement' or name for 'register'. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct EnvironmentTool { /// Action to perform: 'update_fingerprint', 'read_fingerprint', 'log_requirement', 'register', or 'get_details'. @@ -954,14 +980,7 @@ pub enum ClipboardAction { Write, } /// Smart OS Clipboard management with overwrite-immune screenshot caching and OCR. -/// Actions: -/// - 'image': Get latest screenshot image path (Windows + WSL) and extracted verbatim OCR text. /// Returns cached screenshot even if text was copied afterwards! -/// - 'text': Get latest clipboard text (or normalized Markdown if HTML was copied). -/// - 'history': View recent clipboard history ring buffer (images and text with timestamps). -/// - 'clear': Clear OS clipboard and memory cache. -/// - 'write': Write content to OS clipboard. Optional: text, html, files, image_path. -/// /// Triggers: Call 'image' immediately when user says "look at image in clipboard", "see screenshot", /// "look at clipboard", "what I copied", or shares terminal error captures. #[derive(Debug, Deserialize, Serialize, JsonSchema)]