Refactor remaining tools to tagged enums

Refactored DecisionsTool, TechDebtTool, ManageCheckpointTool, HypothesesTool, AgentSignalsTool to use tagged enums for precise JSON schemas, and updated their handlers.
This commit is contained in:
Riz Ashraf committed 2026-10-10 10:23:58 +01:00
1 parent 8ff43487bc
commit 2cfaeda0cb
3 files changed
+438 -497

No files matched your search

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