Compare commits

..
19 changed files with 1660 additions and 1168 deletions

No files matched your search

Generated
+4 -40
View File
@@ -2651,7 +2651,7 @@ dependencies = [
"reqwest", "reqwest",
"rmcp", "rmcp",
"rten", "rten",
"schemars 1.2.2", "schemars",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
@@ -3009,7 +3009,6 @@ dependencies = [
"mcp-stdio", "mcp-stdio",
"rmcp", "rmcp",
"rmpv", "rmpv",
"schemars 0.8.22",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -3860,7 +3859,7 @@ dependencies = [
"pastey 0.2.3", "pastey 0.2.3",
"pin-project-lite", "pin-project-lite",
"rmcp-macros", "rmcp-macros",
"schemars 1.2.2", "schemars",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.20", "thiserror 2.0.20",
@@ -4180,18 +4179,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "schemars"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
dependencies = [
"dyn-clone",
"schemars_derive 0.8.22",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "1.2.2" version = "1.2.2"
@@ -4201,23 +4188,11 @@ dependencies = [
"chrono", "chrono",
"dyn-clone", "dyn-clone",
"ref-cast", "ref-cast",
"schemars_derive 1.2.2", "schemars_derive",
"serde", "serde",
"serde_json", "serde_json",
] ]
[[package]]
name = "schemars_derive"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals 0.29.1",
"syn 2.0.119",
]
[[package]] [[package]]
name = "schemars_derive" name = "schemars_derive"
version = "1.2.2" version = "1.2.2"
@@ -4226,7 +4201,7 @@ checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals 0.30.0", "serde_derive_internals",
"syn 3.0.6", "syn 3.0.6",
] ]
@@ -4301,17 +4276,6 @@ dependencies = [
"syn 3.0.6", "syn 3.0.6",
] ]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "serde_derive_internals" name = "serde_derive_internals"
version = "0.30.0" version = "0.30.0"
+2 -2
View File
@@ -34,9 +34,9 @@ Before making expensive active tool calls, read available MCP resources:
- **Error Fixes**: The moment a tricky, undocumented, or environment-specific bug is resolved, IMMEDIATELY call `log_error_fix`. Supply `repo_name`, `error_category`, and `stack_trace` so future searches can perform embedding-based match via `search_error_fixes`. - **Error Fixes**: The moment a tricky, undocumented, or environment-specific bug is resolved, IMMEDIATELY call `log_error_fix`. Supply `repo_name`, `error_category`, and `stack_trace` so future searches can perform embedding-based match via `search_error_fixes`.
- **Tech Debt**: If you notice an anti-pattern but deliberately skip fixing it to focus on a feature, IMMEDIATELY call `tech_debt` (action: "log") with `description`, `file_path`, `line_range`, `workaround`, `effort_estimate`, and `severity`. - **Tech Debt**: If you notice an anti-pattern but deliberately skip fixing it to focus on a feature, IMMEDIATELY call `tech_debt` (action: "log") with `description`, `file_path`, `line_range`, `workaround`, `effort_estimate`, and `severity`.
- **Architectural Decisions (ADR) & Lifecycle Closure**: - **Architectural Decisions (ADR) & Lifecycle Closure**:
- When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequences`. - When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequence`.
- **MANDATORY Definition of Done**: When code implementing an ADR is committed, you MUST IMMEDIATELY call `decisions` (action: "update", id: "ADR-XXXX", status: "implemented", git_commit: <commit_hash>, git_branch: <branch>). NEVER leave an ADR in `accepted` once the implementing code is committed. The repository also executes `scripts/git-reconcile.py` on post-commit hooks (`just install-git-hooks`) to reconcile commit references automatically. - **MANDATORY Definition of Done**: When code implementing an ADR is committed, you MUST IMMEDIATELY call `decisions` (action: "update", id: "ADR-XXXX", status: "implemented", git_commit: <commit_hash>, git_branch: <branch>). NEVER leave an ADR in `accepted` once the implementing code is committed. The repository also executes `scripts/git-reconcile.py` on post-commit hooks (`just install-git-hooks`) to reconcile commit references automatically.
- **Task Management**: When creating tasks, supply `priority` ('low'|'medium'|'high'|'urgent'), `assigned_agent` (e.g. subagent role), `verification_command` (automated test command), and `criteria`. - **Task Management**: When creating tasks, supply `priority` ('low'|'medium'|'high'|'urgent'), `assigned_agent` (e.g. subagent role), `verification_command` (automated test command), and `acceptance_criteria`.
- **VCS & SVN Agnosticism**: Supply `vcs_type` ('git'|'svn'|'hg'), `vcs_revision` (git hash or svn revision like 'r12345'), and `upstream_url` to `log_code_change` and workspace tools. - **VCS & SVN Agnosticism**: Supply `vcs_type` ('git'|'svn'|'hg'), `vcs_revision` (git hash or svn revision like 'r12345'), and `upstream_url` to `log_code_change` and workspace tools.
- **Terminal & Shell Context**: Terminal sessions and commands are automatically tracked in the server over zero-latency UDP. Query `/terminal/history` or `memory://terminal/recent` when analyzing shell execution context. - **Terminal & Shell Context**: Terminal sessions and commands are automatically tracked in the server over zero-latency UDP. Query `/terminal/history` or `memory://terminal/recent` when analyzing shell execution context.
- **Hypotheses & Root Cause Analysis**: When diagnosing complex bugs or race conditions, call `hypotheses` (action: "log" / "query") to record test evidence and maintain reasoning trails across sessions. - **Hypotheses & Root Cause Analysis**: When diagnosing complex bugs or race conditions, call `hypotheses` (action: "log" / "query") to record test evidence and maintain reasoning trails across sessions.
+1 -1
View File
@@ -26,7 +26,7 @@ Headless mode (`nvim --headless`) is strictly banned for interactive edits.
Headless instances are allowed ONLY for non-interactive background batch processing (e.g., project-wide formatting or linting). Headless instances are allowed ONLY for non-interactive background batch processing (e.g., project-wide formatting or linting).
## 4. UI Presentation & Chat Console Minimization ## 4. UI Presentation & Chat Console Minimization
Never output large plans, context blocks, or architectural discussions to the chat console if Neovim is running. You MUST use the `nvim_buffer` and `nvim_workspace` tools to open the markdown natively in the editor (e.g., `Antigravity_Plan.md` scratch buffer). Reserve the chat console strictly for brief confirmations. Never output large plans, context blocks, or architectural discussions to the chat console if Neovim is running. You MUST use the `nvim_buffer` and `nvim_workspace` tools to open a vertical split (e.g., `Antigravity_Plan.md` scratch buffer) and present the markdown natively. Reserve the chat console strictly for brief confirmations.
## 5. Visual Cues & Auto-Save ## 5. Visual Cues & Auto-Save
When manipulating buffers via MCP: When manipulating buffers via MCP:
-1
View File
@@ -16,7 +16,6 @@ rmcp = { version = "3.4.0", features = ["server"] }
mcp-stdio = { version = "1.0.0", path = "../mcp-stdio" } mcp-stdio = { version = "1.0.0", path = "../mcp-stdio" }
bytes = "1.12.1" bytes = "1.12.1"
dashmap = "6.2.1" dashmap = "6.2.1"
schemars = "0.8"
[build-dependencies] [build-dependencies]
chrono = "0.4.45" chrono = "0.4.45"
+935 -263
View File
File diff suppressed because it is too large. Load diff
-101
View File
@@ -1,101 +0,0 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimBufferTool {
Read {
file: String,
start_line: Option<i64>,
end_line: Option<i64>,
},
Replace {
file: Option<String>,
start_line: Option<i64>,
end_line: Option<i64>,
replacement_content: Option<String>,
expected_content: Option<String>,
edits: Option<Vec<BufferEdit>>,
},
Save {
file: Option<String>,
},
Undo {
steps: Option<i64>,
},
Redo {
steps: Option<i64>,
},
CreateScratch {
name: Option<String>,
content: Option<String>,
},
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct BufferEdit {
pub file: String,
pub start_line: i64,
pub end_line: i64,
pub replacement_content: String,
pub expected_content: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimWorkspaceTool {
ListBuffers,
ListWindows,
Focus {
win_id: i64,
},
Split {
direction: Option<String>,
file: Option<String>,
buf_id: Option<i64>,
},
Cwd {
path: Option<String>,
},
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimIntelligenceTool {
Hover { file: String, line: i64, col: i64 },
Definition { file: String, line: i64, col: i64 },
References { file: String, line: i64, col: i64 },
CodeAction { file: String, line: i64, col: i64 },
Rename { file: String, line: i64, col: i64, new_name: String },
Diagnostics { file: String },
Outline { file: String },
Query { file: String, query: Option<String>, preset: Option<String> },
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimUiTool {
Highlight {
buf_id: Option<i64>,
start_line: i64,
end_line: Option<i64>,
highlight_group: Option<String>,
duration_ms: Option<i64>,
},
GhostText {
line: i64,
text: String,
highlight_group: Option<String>,
},
Clear {
buf_id: Option<i64>,
},
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimExecTool {
Lua { code: String },
Vimscript { code: String },
Terminal { command: String },
}
-25
View File
@@ -1,25 +0,0 @@
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(JsonSchema, Deserialize, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum TasksToolEnum {
/// Create task.
Add {
/// Task title.
title: String,
/// Task description.
description: Option<String>,
},
/// Delete task.
Delete {
/// Task ID.
id: String,
}
}
fn main() {
let schema = schema_for!(TasksToolEnum);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
-6
View File
@@ -1,6 +0,0 @@
use mcp_memory_server::tools::{DecisionsTool, TechDebtTool, ManageCheckpointTool, HypothesesTool, AgentSignalsTool};
use schemars::schema_for;
fn main() {
println!("{}", serde_json::to_string_pretty(&schema_for!(AgentSignalsTool)).unwrap());
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::store::STORE_TABLE; use crate::store::STORE_TABLE;
use redb::Database; use redb::{Database, ReadableTable};
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
+29 -167
View File
@@ -163,15 +163,9 @@ impl McpTool for CreateEntitiesHandler {
let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut inserted = Vec::new(); let mut inserted = Vec::new();
state.modify_graph(|g| { state.modify_graph(|g| {
for entity_input in req.entities { for mut entity in req.entities {
if !entity_input.name.is_empty() { if !entity.name.is_empty() {
let entity = crate::models::Entity { entity.entity_type = crate::models::normalize_entity_type(&entity.entity_type);
name: entity_input.name.clone(),
entity_type: crate::models::normalize_entity_type(&entity_input.entity_type),
observations: entity_input.observations,
namespace: entity_input.namespace.unwrap_or_else(crate::models::default_namespace),
..Default::default()
};
inserted.push(entity.clone()); inserted.push(entity.clone());
g.entities.insert(entity.name.clone(), entity); g.entities.insert(entity.name.clone(), entity);
} }
@@ -193,108 +187,6 @@ impl McpTool for CreateEntitiesHandler {
} }
} }
pub struct UpdateEntitiesHandler;
#[async_trait]
impl McpTool for UpdateEntitiesHandler {
fn name(&self) -> &'static str {
"update_entities"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<UpdateEntitiesTool>(
"update_entities",
"Update existing entities in the knowledge graph (e.g., to rename an entity or change its type).",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: UpdateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut updated_entities = Vec::new();
let mut deleted_names = Vec::new();
let mut not_found = Vec::new();
let mut conflict_names = Vec::new();
state.modify_graph(|g| {
for update in req.updates {
if !g.entities.contains_key(&update.name) {
not_found.push(update.name.clone());
continue;
}
if let Some(new_name) = &update.new_name {
if update.name != *new_name && g.entities.contains_key(new_name) {
conflict_names.push(new_name.clone());
continue;
}
}
if let Some(mut entity) = g.entities.remove(&update.name) {
let mut renamed = false;
let old_name = entity.name.clone();
if let Some(new_name) = update.new_name {
if old_name != new_name {
entity.name = new_name.clone();
renamed = true;
deleted_names.push(old_name.clone());
}
}
if let Some(new_type) = update.new_entity_type {
entity.entity_type = crate::models::normalize_entity_type(&new_type);
}
let current_name = entity.name.clone();
if renamed {
// update relations
let mut seen = std::collections::HashSet::new();
g.relations.retain_mut(|r| {
if r.from == old_name {
r.from = current_name.clone();
}
if r.to == old_name {
r.to = current_name.clone();
}
// Keep unique relations
seen.insert(r.clone())
});
}
updated_entities.push(entity.clone());
g.entities.insert(current_name, entity);
}
}
});
if !not_found.is_empty() {
return Err(crate::error::AppError::Internal(format!(
"Error: Entities not found: {}", not_found.join(", ")
)));
}
if !conflict_names.is_empty() {
return Err(crate::error::AppError::Internal(format!(
"Error: Cannot rename to existing entity names: {}", conflict_names.join(", ")
)));
}
let idx = state.get_search_index().await;
for old_name in deleted_names {
drop(idx.delete_document(&old_name));
}
if !updated_entities.is_empty() {
let _ = idx.index_entities_batch(&updated_entities).await;
}
let names: Vec<String> = updated_entities.iter().map(|e| e.name.clone()).collect();
Ok(format!("Successfully updated {} entity/entities: {}", names.len(), names.join(", ")))
}
}
pub struct CreateRelationsHandler; pub struct CreateRelationsHandler;
#[async_trait] #[async_trait]
@@ -364,15 +256,10 @@ impl McpTool for CreateRelationsHandler {
auto_created.push(node_name); auto_created.push(node_name);
} }
} }
for relation_input in req.relations { for mut relation in req.relations {
if !relation_input.from.is_empty() && !relation_input.to.is_empty() { if !relation.from.is_empty() && !relation.to.is_empty() {
let relation = crate::models::Relation { relation.relation_type =
from: relation_input.from, crate::models::normalize_relation_type(&relation.relation_type);
to: relation_input.to,
relation_type: crate::models::normalize_relation_type(&relation_input.relation_type),
namespace: relation_input.namespace.unwrap_or_else(crate::models::default_namespace),
..Default::default()
};
added_relations.push(format!( added_relations.push(format!(
"{} -[{}]-> {}", "{} -[{}]-> {}",
relation.from, relation.relation_type, relation.to relation.from, relation.relation_type, relation.to
@@ -511,48 +398,30 @@ impl McpTool for DeleteObservationsHandler {
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: DeleteObservationsTool = let req: DeleteObservationsTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing = Vec::new();
let mut missing_entities = Vec::new(); state.read_graph(|g| {
let mut missing_observations = Vec::new(); for d in &req.deletions {
if !g.entities.contains_key(&d.entity_name) {
state.modify_graph(|master| { missing.push(d.entity_name.clone());
for d in req.deletions {
if let Some(e) = master.entities.get_mut(&d.entity_name) {
for target in d.observations {
let target_clean = target.trim();
if target_clean.is_empty() { continue; }
let initial_len = e.observations.len();
e.observations.retain(|o| {
let o_clean = o.trim();
// Exact match OR substantial substring match (to forgive LLM whitespace/newline errors)
!(o_clean == target_clean || (target_clean.len() >= 10 && o_clean.contains(target_clean)))
});
if e.observations.len() == initial_len {
missing_observations.push(target);
}
}
} else {
missing_entities.push(d.entity_name);
} }
} }
}); });
if !missing_entities.is_empty() { if !missing.is_empty() {
return Err(crate::error::AppError::Internal(format!( return Err(crate::error::AppError::Internal(format!(
"Error: Entities not found: {}. Please use search_nodes to verify the exact entity names.", "Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.",
missing_entities.join(", ") missing.join(", ")
)));
}
if !missing_observations.is_empty() {
return Err(crate::error::AppError::Internal(format!(
"Error: Failed to find {} observation(s) to delete. Make sure you provide a sufficiently long exact substring match.",
missing_observations.len()
))); )));
} }
state.modify_graph(|master| {
for d in req.deletions {
if let Some(e) = master.entities.get_mut(&d.entity_name) {
let to_rem: HashSet<_> = d.observations.into_iter().collect();
e.observations.retain(|o| !to_rem.contains(o));
}
}
});
Ok("Observations deleted".to_string()) Ok("Observations deleted".to_string())
} }
} }
@@ -574,21 +443,13 @@ impl McpTool for DeleteRelationsHandler {
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: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut deleted_count = 0; let mut missing_count = 0;
let requested_count = req.relations.len();
state.modify_graph(|master| { state.modify_graph(|master| {
let to_rem: HashSet<_> = req.relations.into_iter().collect();
let initial_len = master.relations.len(); let initial_len = master.relations.len();
master.relations.retain(|r| { master.relations.retain(|r| !to_rem.contains(r));
let should_delete = req.relations.iter().any(|target| { missing_count = to_rem.len() - (initial_len - master.relations.len());
target.from == r.from && target.to == r.to && target.relation_type == r.relation_type
});
!should_delete
});
deleted_count = initial_len - master.relations.len();
}); });
let missing_count = requested_count.saturating_sub(deleted_count);
if missing_count > 0 { if missing_count > 0 {
return Err(crate::error::AppError::Internal(format!( return Err(crate::error::AppError::Internal(format!(
"Error: {} relation(s) not found in graph. Please verify exact relation properties (from, to, relation_type) using read_graph or get_subgraph.", "Error: {} relation(s) not found in graph. Please verify exact relation properties (from, to, relation_type) using read_graph or get_subgraph.",
@@ -1508,9 +1369,10 @@ mod tests {
.unwrap(); .unwrap();
assert!(res.contains("Successfully created 1 relation(s)")); assert!(res.contains("Successfully created 1 relation(s)"));
// Test serde field aliases (source/target/relationType mapped to from/to/relation_type)
let alias_args = json!({ let alias_args = json!({
"relations": [ "relations": [
{"from": "A", "to": "B", "relation_type": "knows"} {"source": "A", "target": "B", "relationType": "knows"}
] ]
}); });
let alias_res = handler.execute(alias_args, state.clone()).await.unwrap(); let alias_res = handler.execute(alias_args, state.clone()).await.unwrap();
+172 -98
View File
@@ -24,7 +24,24 @@ impl McpTool for LogErrorFixHandler {
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: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution); let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
let solution = req.solution; let mut solution = req.solution;
if state.ollama.is_available().await {
let prompt = format!(
"Analyze this error signature and solution. Output 1 sentence summarizing the root cause and fix:\nSignature: {}\nSolution: {}",
req.signature, solution
);
if let Ok(summary) = state
.ollama
.generate(&prompt, Some(&state.ollama.reasoning_model), None, None)
.await
{
let clean = summary.trim();
if !clean.is_empty() {
solution = format!("{} (AI Analysis: {})", solution, clean);
}
}
}
let embedding = crate::embedding::generate_embedding_async(text_to_embed) let embedding = crate::embedding::generate_embedding_async(text_to_embed)
.await .await
@@ -187,6 +204,18 @@ impl McpTool for LogCodeChangeHandler {
description = format!("{} [Symbols: {}]", description, symbols.join(", ")); description = format!("{} [Symbols: {}]", description, symbols.join(", "));
} }
if state.ollama.is_available().await {
let prompt = format!(
"Summarize in 1 concise sentence the architectural impact of changing file '{}': {}",
req.file_path, description
);
if let Ok(summary) = state.ollama.generate(&prompt, None, None, None).await {
let clean = summary.trim();
if !clean.is_empty() {
description = format!("{} (AI Summary: {})", description, clean);
}
}
}
let change_kind = match req let change_kind = match req
.change_kind .change_kind
.as_deref() .as_deref()
@@ -354,14 +383,18 @@ 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())?;
match req { let ns = req
DecisionsTool::Log { title, status, context, decision, consequences, namespace, repo_name, author, affected_components, alternatives_considered, supersedes, git_commit, git_branch, task_id } => { .namespace
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace); .unwrap_or_else(crate::models::default_namespace);
let title = title; match req.action {
let status = status.unwrap_or_else(|| "accepted".to_string()); DecisionAction::Log => {
let context = context.unwrap_or_default(); let title = req.title.ok_or_else(|| {
let decision = decision.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 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())
@@ -374,7 +407,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) = &supersedes { if let Some(superseded_id) = &req.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();
@@ -392,17 +425,17 @@ impl McpTool for DecisionsHandler {
decision: decision.clone(), decision: decision.clone(),
consequence, consequence,
status, status,
supersedes: supersedes, supersedes: req.supersedes,
timestamp: crate::handlers::utils::now_secs(), timestamp: crate::handlers::utils::now_secs(),
namespace: ns, namespace: ns,
repo_name: repo_name, repo_name: req.repo_name,
alternatives_considered: alternatives_considered.unwrap_or_default(), alternatives_considered: req.alternatives_considered.unwrap_or_default(),
affected_components: affected_components.unwrap_or_default(), affected_components: req.affected_components.unwrap_or_default(),
author: author, author: req.author,
git_commit: git_commit, git_commit: req.git_commit,
git_branch: git_branch, git_branch: req.git_branch,
resolved_at, resolved_at,
task_id: task_id, task_id: req.task_id,
}; };
adrs_to_index.push(a.clone()); adrs_to_index.push(a.clone());
@@ -420,15 +453,17 @@ impl McpTool for DecisionsHandler {
); );
Ok(format!("Logged decision {}: {}", final_id, title)) Ok(format!("Logged decision {}: {}", final_id, title))
} }
DecisionsTool::Update { id, title, status, context, decision, consequences, git_commit, git_branch, task_id, namespace: _, repo_name, author, affected_components, alternatives_considered, supersedes } => { DecisionAction::Update => {
let id = id; 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())
})?;
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) = &supersedes if let Some(superseded_id) = &req.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))
@@ -439,19 +474,19 @@ impl McpTool for DecisionsHandler {
} }
let a = &mut adrs[pos]; let a = &mut adrs[pos];
if let Some(t) = title { if let Some(t) = req.title {
a.title = t; a.title = t;
} }
if let Some(c) = context { if let Some(c) = req.context {
a.context = c; a.context = c;
} }
if let Some(d) = decision { if let Some(d) = req.decision {
a.decision = d; a.decision = d;
} }
if let Some(cons) = consequences { if let Some(cons) = req.consequences {
a.consequence = cons; a.consequence = cons;
} }
if let Some(s) = status { if let Some(s) = req.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()
@@ -462,29 +497,29 @@ impl McpTool for DecisionsHandler {
} }
a.status = s; a.status = s;
} }
if supersedes.is_some() { if req.supersedes.is_some() {
a.supersedes = supersedes; a.supersedes = req.supersedes;
} }
if repo_name.is_some() { if req.repo_name.is_some() {
a.repo_name = repo_name; a.repo_name = req.repo_name;
} }
if let Some(alts) = alternatives_considered { if let Some(alts) = req.alternatives_considered {
a.alternatives_considered = alts; a.alternatives_considered = alts;
} }
if let Some(aff) = affected_components { if let Some(aff) = req.affected_components {
a.affected_components = aff; a.affected_components = aff;
} }
if author.is_some() { if req.author.is_some() {
a.author = author; a.author = req.author;
} }
if git_commit.is_some() { if req.git_commit.is_some() {
a.git_commit = git_commit; a.git_commit = req.git_commit;
} }
if git_branch.is_some() { if req.git_branch.is_some() {
a.git_branch = git_branch; a.git_branch = req.git_branch;
} }
if task_id.is_some() { if req.task_id.is_some() {
a.task_id = task_id; a.task_id = req.task_id;
} }
adrs_to_index.push(a.clone()); adrs_to_index.push(a.clone());
@@ -513,10 +548,9 @@ impl McpTool for DecisionsHandler {
))) )))
} }
} }
DecisionsTool::Query { query, namespace, limit, include_body } => { DecisionAction::Query => {
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace); let limit = req.limit.unwrap_or(20);
let limit = limit.unwrap_or(20); let include_body = req.include_body.unwrap_or(true);
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()
@@ -524,7 +558,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) = &query { if let Some(q) = &req.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,
@@ -567,8 +601,10 @@ impl McpTool for DecisionsHandler {
})?; })?;
Ok(data) Ok(data)
} }
DecisionsTool::Delete { id } => { DecisionAction::Delete => {
let id = id; 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())
})?;
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) {
@@ -608,11 +644,15 @@ 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())?;
match req { let ns = req
TechDebtTool::Log { description, title: _, ideal_solution, git_commit, git_branch, symbol_references: _, line_range, namespace, severity, repo_name, file_path, workaround, effort_estimate } => { .namespace
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace); .unwrap_or_else(crate::models::default_namespace);
let desc = description; match req.action {
let ideal = ideal_solution.unwrap_or_default(); 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();
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
@@ -625,15 +665,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: git_commit, git_commit: req.git_commit,
git_branch: git_branch, git_branch: req.git_branch,
embedding, embedding,
repo_name: repo_name, repo_name: req.repo_name,
severity: severity, severity: req.severity,
file_path: file_path, file_path: req.file_path,
line_range: line_range, line_range: req.line_range,
workaround: workaround, workaround: req.workaround,
effort_estimate: effort_estimate, effort_estimate: req.effort_estimate,
}); });
if debts.len() > 300 { if debts.len() > 300 {
let severity_rank = let severity_rank =
@@ -657,8 +697,10 @@ impl McpTool for TechDebtHandler {
}); });
Ok("Tech debt logged".to_string()) Ok("Tech debt logged".to_string())
} }
TechDebtTool::Resolve { id } => { TechDebtAction::Resolve => {
let id = id; 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())
})?;
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() {
@@ -678,10 +720,9 @@ impl McpTool for TechDebtHandler {
)) ))
} }
} }
TechDebtTool::List { include_resolved, namespace, summary_level, max_tokens } => { TechDebtAction::List => {
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace); let inc = req.include_resolved.unwrap_or(false);
let inc = include_resolved.unwrap_or(false); let level = req.summary_level.as_deref().unwrap_or("detailed");
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()
@@ -702,7 +743,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) = max_tokens { if let Some(max_t) = req.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);
@@ -1158,9 +1199,13 @@ 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 { match req.action {
ManageCheckpointTool::Create { name_or_id, description, namespace } => { CheckpointAction::Create => {
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?; let name = req.name_or_id.ok_or_else(|| {
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) {
@@ -1185,12 +1230,15 @@ 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) = &description { if let Some(desc) = &req.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 = namespace.clone().unwrap_or_else(|| "global".to_string()); let ns = req
.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(),
@@ -1203,8 +1251,12 @@ impl McpTool for ManageCheckpointHandler {
Ok(format!("Checkpoint '{}' created successfully.", name)) Ok(format!("Checkpoint '{}' created successfully.", name))
} }
ManageCheckpointTool::Restore { name_or_id, namespace: _ } => { CheckpointAction::Restore => {
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?; let name = req.name_or_id.ok_or_else(|| {
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() {
@@ -1245,7 +1297,7 @@ impl McpTool for ManageCheckpointHandler {
Ok(format!("Checkpoint '{}' restored successfully.", name)) Ok(format!("Checkpoint '{}' restored successfully.", name))
} }
ManageCheckpointTool::List { namespace: _ } => { CheckpointAction::List => {
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) {
@@ -1263,8 +1315,12 @@ impl McpTool for ManageCheckpointHandler {
} }
Ok(serde_yaml::to_string(&list)?) Ok(serde_yaml::to_string(&list)?)
} }
ManageCheckpointTool::Delete { name_or_id, namespace: _ } => { CheckpointAction::Delete => {
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?; let name = req.name_or_id.ok_or_else(|| {
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);
@@ -1460,9 +1516,14 @@ 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 {
HypothesesTool::Log { hypothesis, task_id, status, evidence } => { match req.action {
let hyp_text = hypothesis; HypothesisAction::Log => {
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()
@@ -1471,10 +1532,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: task_id, task_id: req.task_id,
hypothesis: hyp_text, hypothesis: hyp_text,
status: status.unwrap_or_else(|| "unverified".to_string()), status: req.status.unwrap_or_else(|| "unverified".to_string()),
evidence: evidence, evidence: req.evidence,
timestamp, timestamp,
..Default::default() ..Default::default()
}; };
@@ -1483,17 +1544,17 @@ impl McpTool for HypothesesHandler {
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id)) Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
} }
HypothesesTool::Query { query, task_id, status: _ } => { HypothesisAction::Query => {
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) = &task_id if let Some(tid) = &req.task_id
&& h.task_id.as_ref() != Some(tid) && h.task_id.as_ref() != Some(tid)
{ {
return false; return false;
} }
if let Some(q) = &query { if let Some(q) = &req.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
@@ -1604,11 +1665,24 @@ 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 {
AgentSignalsTool::Broadcast { sender, signal_type, payload, ttl_seconds } => { match req.action {
let sender = sender; AgentSignalAction::Broadcast => {
let signal_type = signal_type; let sender = req.sender.ok_or_else(|| {
let payload = payload; 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(),
)
})?;
let timestamp = std::time::SystemTime::now() let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
@@ -1622,7 +1696,7 @@ impl McpTool for AgentSignalsHandler {
signal_type: signal_type.clone(), signal_type: signal_type.clone(),
payload, payload,
timestamp, timestamp,
ttl_seconds: ttl_seconds, ttl_seconds: req.ttl_seconds,
..Default::default() ..Default::default()
}; };
@@ -1650,7 +1724,7 @@ impl McpTool for AgentSignalsHandler {
sig_id, sender sig_id, sender
)) ))
} }
AgentSignalsTool::Query { sender, signal_type, limit } => { AgentSignalAction::Query => {
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()
@@ -1665,19 +1739,19 @@ impl McpTool for AgentSignalsHandler {
{ {
return false; return false;
} }
if let Some(sender) = &sender if let Some(sender) = &req.sender
&& s.sender.to_lowercase() != sender.to_lowercase() && s.sender.to_lowercase() != sender.to_lowercase()
{ {
return false; return false;
} }
if let Some(st) = &signal_type if let Some(st) = &req.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(limit.unwrap_or(20)) .take(req.limit.unwrap_or(20))
.cloned() .cloned()
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
@@ -1826,7 +1900,7 @@ mod tests {
"title": "Architecture", "title": "Architecture",
"context": "Needs DB", "context": "Needs DB",
"decision": "Use SQLite", "decision": "Use SQLite",
"consequences": "Simple", "consequence": "Simple",
}); });
let res1 = decision_handler let res1 = decision_handler
.execute(args_dec, state.clone()) .execute(args_dec, state.clone())
@@ -2038,7 +2112,7 @@ mod tests {
"title": "Use Axum", "title": "Use Axum",
"context": "Architecture choice", "context": "Architecture choice",
"decision": "Adopt Axum for web framework", "decision": "Adopt Axum for web framework",
"consequences": "Fast async API routing" "consequence": "Fast async API routing"
}), }),
state.clone(), state.clone(),
) )
+68 -35
View File
@@ -23,12 +23,15 @@ 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 { match req.action {
TasksTool::Add { title, description, parent_id, dependencies, git_branch, namespace, repo_name, priority, assigned_agent, verification_command } => { TaskAction::Add => {
let description = description.unwrap_or_default(); 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();
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 = dependencies.unwrap_or_default(); let deps = req.dependencies.unwrap_or_default();
let task = Task { let task = Task {
id: task_id.clone(), id: task_id.clone(),
@@ -37,16 +40,16 @@ impl McpTool for TasksHandler {
description, description,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
git_branch, git_branch: req.git_branch,
parent_id, parent_id: req.parent_id,
dependencies: deps, dependencies: deps,
acceptance_criteria: vec![], acceptance_criteria: vec![],
expires_at: None, expires_at: None,
namespace: namespace.unwrap_or_else(|| "default".to_string()), namespace: req.namespace.unwrap_or_else(|| "default".to_string()),
repo_name, repo_name: req.repo_name,
priority, priority: req.priority,
assigned_agent, assigned_agent: req.assigned_agent,
verification_command, verification_command: req.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));
@@ -72,7 +75,13 @@ impl McpTool for TasksHandler {
}); });
Ok(format!("Task added with ID: {}", task_id)) Ok(format!("Task added with ID: {}", task_id))
} }
TasksTool::Update { id, status } => { 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())
})?;
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;
@@ -193,7 +202,10 @@ impl McpTool for TasksHandler {
)) ))
} }
} }
TasksTool::Delete { id } => { 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())
})?;
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| {
@@ -255,19 +267,19 @@ impl McpTool for TasksHandler {
)) ))
} }
} }
TasksTool::List { git_branch, status, summary_level, max_tokens, offset } => { TaskAction::List => {
let level = summary_level.as_deref().unwrap_or("detailed"); let level = req.summary_level.as_deref().unwrap_or("detailed");
let offset = offset.unwrap_or(0); let offset = req.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 status.as_deref() { let status_match = match req.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 &git_branch { let branch_match = match &req.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,
}; };
@@ -285,7 +297,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) = max_tokens { if let Some(max_t) = req.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();
@@ -296,7 +308,13 @@ impl McpTool for TasksHandler {
})?; })?;
Ok(data) Ok(data)
} }
TasksTool::SetCriteria { id, criteria: criteria_list } => { 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())
})?;
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| {
@@ -337,8 +355,11 @@ impl McpTool for TasksHandler {
)) ))
} }
} }
TasksTool::Verify { id, proof } => { TaskAction::Verify => {
let proof_str = proof.unwrap_or_else(|| "Verified".to_string()); 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());
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;
@@ -415,29 +436,41 @@ 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 { match req.action {
MilestonesTool::Add { title, namespace, target_date, description, deliverables, task_ids, repo_name } => { MilestoneAction::Add => {
let ns = namespace.unwrap_or_else(crate::models::default_namespace); 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);
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: target_date target_date: req
.target_date
.as_deref() .as_deref()
.and_then(|s| s.parse::<u64>().ok()), .and_then(|s| s.parse::<u64>().ok()),
description: description, description: req.description,
task_ids: task_ids.unwrap_or_default(), task_ids: req.task_ids.unwrap_or_default(),
deliverables: deliverables.unwrap_or_default(), deliverables: req.deliverables.unwrap_or_default(),
repo_name, repo_name: req.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())
} }
MilestonesTool::Update { id, status, deliverables, task_ids } => { 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())
})?;
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() {
@@ -449,9 +482,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) = &task_ids { if let Some(task_ids) = &req.task_ids {
m.task_ids = task_ids.clone(); m.task_ids = task_ids.clone();
if let Some(deliverables) = &deliverables { if let Some(deliverables) = &req.deliverables {
m.deliverables = deliverables.clone(); m.deliverables = deliverables.clone();
} }
} }
@@ -469,12 +502,12 @@ impl McpTool for MilestonesHandler {
)) ))
} }
} }
MilestonesTool::List { namespace } => { MilestoneAction::List => {
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) = &namespace { if let Some(ns) = &req.namespace {
&i.namespace == ns &i.namespace == ns
} else { } else {
true true
+59 -44
View File
@@ -285,60 +285,75 @@ pub async fn memory_consolidation_worker(state: Arc<MemoryState>) {
_ = interval.tick() => {}, _ = interval.tick() => {},
} }
let entities: Vec<String> = state.graph.read_with(|g| { let entities: Vec<_> = state.graph.read_with(|g| {
g.entities.keys().cloned().collect() g.entities
.values()
.map(|e| (e.name.clone(), e.entity_type.clone()))
.collect()
}); });
if entities.len() < 2 { if entities.len() < 2 {
continue; continue;
} }
let mut duplicates = None; let mut entity_summaries = String::new();
let mut seen = std::collections::HashMap::new(); for (name, e_type) in entities.iter().take(50) {
for name in &entities { entity_summaries.push_str(&format!("- [{}] {}\n", e_type, name));
let normalized: String = name
.chars()
.filter(|c| c.is_alphanumeric())
.flat_map(|c| c.to_lowercase())
.collect();
// Skip empty normalized names
if normalized.is_empty() {
continue;
}
if let Some(existing) = seen.insert(normalized, name.clone()) {
if existing != *name {
duplicates = Some((existing, name.clone()));
break;
}
}
} }
if let Some((e1_name, e2_name)) = duplicates { let prompt = format!(
tracing::info!( "Analyze the following list of entities and identify exactly TWO that represent the exact same concept or item but have slightly different names (e.g. 'auth_service' and 'AuthService'). Return ONLY a valid JSON array containing exactly two strings: the two names to merge. If no obvious duplicates exist, return an empty array []. Do not output any markdown formatting or extra text.\n\nEntities:\n{}",
"Memory Consolidation Daemon: Merging '{}' into '{}'", entity_summaries
e2_name, );
e1_name
);
state.modify_graph(|g| {
if let Some(mut e2) = g.entities.remove(&e2_name) {
if let Some(e1) = g.entities.get_mut(&e1_name) {
e1.observations.append(&mut e2.observations);
} else {
g.entities.insert(e2_name.clone(), e2);
}
}
for rel in g.relations.iter_mut() { if let Ok(response) = state
if rel.from == e2_name { .ollama
rel.from = e1_name.clone(); .generate(
} &prompt,
if rel.to == e2_name { None,
rel.to = e1_name.clone(); Some("You are a helpful JSON-only data deduplication assistant. Output only JSON."),
} Some("json"),
)
.await
{
let cleaned = response
.trim()
.trim_start_matches("```json")
.trim_start_matches("```")
.trim_end_matches("```")
.trim();
if let Ok(duplicates) = serde_json::from_str::<Vec<String>>(cleaned)
&& duplicates.len() == 2
{
let e1_name = &duplicates[0];
let e2_name = &duplicates[1];
if e1_name != e2_name {
tracing::info!(
"Memory Consolidation Daemon: Merging '{}' into '{}'",
e2_name,
e1_name
);
state.modify_graph(|g| {
if let Some(mut e2) = g.entities.remove(e2_name) {
if let Some(e1) = g.entities.get_mut(e1_name) {
e1.observations.append(&mut e2.observations);
} else {
g.entities.insert(e2_name.clone(), e2);
}
}
for rel in g.relations.iter_mut() {
if rel.from == *e2_name {
rel.from = e1_name.clone();
}
if rel.to == *e2_name {
rel.to = e1_name.clone();
}
}
});
} }
}); }
} }
} }
} }
+1 -9
View File
@@ -22,7 +22,7 @@ pub fn error(id: serde_json::Value, code: i32, message: &str) -> serde_json::Val
pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::Value { pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::Value {
let schema = schemars::schema_for!(T); let schema = schemars::schema_for!(T);
let mut schema_val = serde_json::to_value(&schema).unwrap_or_default(); let schema_val = serde_json::to_value(&schema).unwrap_or_default();
let desc = schema_val let desc = schema_val
.get("description") .get("description")
@@ -31,14 +31,6 @@ pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::V
.unwrap_or(fallback_desc) .unwrap_or(fallback_desc)
.to_string(); .to_string();
// MCP clients strictly expect a top-level `type: "object"` for tool schemas.
// Tagged enums usually generate a root `oneOf` or `anyOf`.
if let Some(obj) = schema_val.as_object_mut() {
if !obj.contains_key("type") && (obj.contains_key("oneOf") || obj.contains_key("anyOf")) {
obj.insert("type".to_string(), json!("object"));
}
}
// MCP expects standard JSON schema. Schemars returns draft-07. // MCP expects standard JSON schema. Schemars returns draft-07.
json!({ json!({
"name": name, "name": name,
+5
View File
@@ -185,6 +185,7 @@ impl std::fmt::Display for TaskStatus {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct Entity { pub struct Entity {
pub name: String, pub name: String,
#[serde(alias = "entityType", alias = "type", alias = "category")]
pub entity_type: String, pub entity_type: String,
#[serde(default)] #[serde(default)]
pub observations: Vec<String>, pub observations: Vec<String>,
@@ -206,8 +207,11 @@ pub struct Entity {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct Relation { pub struct Relation {
#[serde(alias = "source", alias = "from_node", alias = "source_node")]
pub from: String, pub from: String,
#[serde(alias = "target", alias = "to_node", alias = "target_node")]
pub to: String, pub to: String,
#[serde(alias = "relationType", alias = "type", alias = "relation")]
pub relation_type: String, pub relation_type: String,
#[serde(default = "default_namespace")] #[serde(default = "default_namespace")]
pub namespace: String, pub namespace: String,
@@ -359,6 +363,7 @@ pub struct Adr {
pub title: String, pub title: String,
pub context: String, pub context: String,
pub decision: String, pub decision: String,
#[serde(alias = "consequences")]
pub consequence: String, pub consequence: String,
#[serde(default = "default_adr_status")] #[serde(default = "default_adr_status")]
pub status: String, pub status: String,
-1
View File
@@ -479,7 +479,6 @@ impl MemoryHandler {
register!(graph::QueryGraphPathHandler); register!(graph::QueryGraphPathHandler);
register!(graph::CreateEntitiesHandler); register!(graph::CreateEntitiesHandler);
register!(graph::UpdateEntitiesHandler);
register!(graph::CreateRelationsHandler); register!(graph::CreateRelationsHandler);
register!(graph::AddObservationsHandler); register!(graph::AddObservationsHandler);
register!(graph::DeleteEntitiesHandler); register!(graph::DeleteEntitiesHandler);
+378 -370
View File
@@ -2,60 +2,23 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Create new entities in the knowledge graph. Note: entity_type MUST ALWAYS be PascalCase (e.g. DatabaseTable, McpTool, File). /// Create new entities in the knowledge graph. Note: entity_type MUST ALWAYS be PascalCase (e.g. DatabaseTable, McpTool, File).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct EntityInput {
pub name: String,
pub entity_type: String,
#[serde(default)]
pub observations: Vec<String>,
#[serde(default)]
pub namespace: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateEntitiesTool { pub struct CreateEntitiesTool {
/// Array of entities to create. /// Array of entities to create.
pub entities: Vec<EntityInput>, pub entities: Vec<crate::models::Entity>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UpdateEntityInput {
/// The current name of the entity to update.
pub name: String,
/// Optional new name to rename the entity to.
pub new_name: Option<String>,
/// Optional new entity type (e.g. PascalCase).
pub new_entity_type: Option<String>,
}
/// Update existing entities in the knowledge graph (e.g., to rename an entity or change its type).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UpdateEntitiesTool {
/// Array of entity updates to perform.
pub updates: Vec<UpdateEntityInput>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct RelationInput {
pub from: String,
pub to: String,
pub relation_type: String,
#[serde(default)]
pub namespace: Option<String>,
} }
/// Create new relations between entities in the knowledge graph. /// Create new relations between entities in the knowledge graph.
/// Create new relations between entities in the knowledge graph (accepts 'from' or 'source', 'to' or 'target', and 'relation_type' or 'type').
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateRelationsTool { pub struct CreateRelationsTool {
/// Array of relations to create. /// Array of relations to create. Each relation item MUST use 'from', 'to', and 'relation_type'.
pub relations: Vec<RelationInput>, pub relations: Vec<crate::models::Relation>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ObservationInput { pub struct ObservationInput {
/// The name of the entity.
pub entity_name: String, pub entity_name: String,
/// The observation contents.
pub contents: Vec<String>, pub contents: Vec<String>,
} }
@@ -75,9 +38,7 @@ pub struct DeleteEntitiesTool {
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteObservationInput { pub struct DeleteObservationInput {
/// The name of the entity.
pub entity_name: String, pub entity_name: String,
/// The observations to delete.
pub observations: Vec<String>, pub observations: Vec<String>,
} }
@@ -92,7 +53,7 @@ pub struct DeleteObservationsTool {
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteRelationsTool { pub struct DeleteRelationsTool {
/// Array of relations to delete. /// Array of relations to delete.
pub relations: Vec<RelationInput>, pub relations: Vec<crate::models::Relation>,
} }
/// Read the entire knowledge graph. WARNING: For large graphs, use search_nodes or pagination (limit, offset) to avoid context limits. /// Read the entire knowledge graph. WARNING: For large graphs, use search_nodes or pagination (limit, offset) to avoid context limits.
@@ -221,38 +182,26 @@ 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)]
#[serde(tag = "action", rename_all = "snake_case")] pub struct ManageCheckpointTool {
pub enum ManageCheckpointTool { /// Action to perform: 'create', 'restore', 'list', or 'delete'.
/// Save a checkpoint. pub action: CheckpointAction,
Create { /// Unique name or ID for the snapshot checkpoint.
/// Unique name or ID for the snapshot checkpoint. pub name_or_id: Option<String>,
name_or_id: Option<String>, /// Optional description of why this checkpoint was created.
/// Optional description of why this checkpoint was created. pub description: Option<String>,
description: Option<String>, /// Optional namespace filter.
/// Optional namespace filter. pub namespace: Option<String>,
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.
@@ -364,19 +313,14 @@ pub struct QueryGraphPathTool {
/// Define a strict checklist of acceptance criteria for a given task or feature before starting work. /// Define a strict checklist of acceptance criteria for a given task or feature before starting work.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SetAcceptanceCriteriaTool { pub struct SetAcceptanceCriteriaTool {
/// The title of the task.
pub task_title: String, pub task_title: String,
/// A list of acceptance criteria requirements.
pub criteria: Vec<String>, pub criteria: Vec<String>,
} }
/// Mark a previously defined acceptance criteria as met by providing cryptographic-like proof. /// Mark a previously defined acceptance criteria as met by providing cryptographic-like proof.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct VerifyAcceptanceCriteriaTool { pub struct VerifyAcceptanceCriteriaTool {
/// The ID of the task.
pub task_id: String, pub task_id: String,
/// The specific criterion being verified.
pub criteria: String, pub criteria: String,
/// The proof or evidence that the criterion is met.
pub proof: String, pub proof: String,
} }
@@ -407,30 +351,28 @@ 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)]
#[serde(tag = "action", rename_all = "snake_case")] pub struct HypothesesTool {
pub enum HypothesesTool { /// Action to perform: 'log' or 'query'.
/// Log a new diagnostic hypothesis. pub action: HypothesisAction,
Log { /// Optional task ID associated with this hypothesis or to filter hypotheses.
/// The diagnostic hypothesis or potential root cause. pub task_id: Option<String>,
hypothesis: String, /// The diagnostic hypothesis or potential root cause (required for action 'log').
/// Optional task ID associated with this hypothesis. pub hypothesis: Option<String>,
task_id: Option<String>, /// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'.
/// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'. pub status: Option<String>,
status: Option<String>, /// Evidence or test results supporting or disproving the hypothesis.
/// Evidence or test results supporting or disproving the hypothesis. pub evidence: Option<String>,
evidence: Option<String>, /// Optional search query text (for action 'query').
}, 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 {
@@ -492,35 +434,27 @@ pub struct ProcessLogsTool {
/// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions) /// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions)
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadFileSkeletonTool { pub struct ReadFileSkeletonTool {
/// The path to the file to read.
pub file_path: String, pub file_path: String,
} }
/// Replace a specific AST node in a file (robust structural editing). Use this instead of regex or line-based string replacement to prevent indentation bugs and matching failures. /// Replace a specific AST node in a file (robust structural editing). Use this instead of regex or line-based string replacement to prevent indentation bugs and matching failures.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReplaceAstNodeTool { pub struct ReplaceAstNodeTool {
/// The path of the file to modify.
pub file_path: String, pub file_path: String,
/// The AST node type to replace (e.g., 'function_item', 'impl_item'). pub node_type: String, // e.g., "function_item", "impl_item"
pub node_type: String, pub node_name: String, // e.g., "execute"
/// The name of the specific AST node to replace (e.g., 'execute').
pub node_name: String,
/// The completely new structural content for this AST node.
pub new_content: String, pub new_content: String,
} }
/// Semantic code search using local vector embeddings. Use this conceptual search instead of raw regex (grep) when trying to locate abstract logic or exploring new patterns. /// Semantic code search using local vector embeddings. Use this conceptual search instead of raw regex (grep) when trying to locate abstract logic or exploring new patterns.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SemanticCodeSearchTool { pub struct SemanticCodeSearchTool {
/// The conceptual or semantic search query.
pub query: String, pub query: String,
/// Optional directory to restrict the search to.
pub directory: Option<String>, pub directory: Option<String>,
} }
/// Get a bird's-eye view of directory architecture. Use this when first exploring a new repository to get a summary of what each file is responsible for, instead of blindly reading files. /// Get a bird's-eye view of directory architecture. Use this when first exploring a new repository to get a summary of what each file is responsible for, instead of blindly reading files.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadDirectoryArchitectureTool { pub struct ReadDirectoryArchitectureTool {
/// The directory path to analyze.
pub directory: String, pub directory: String,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
@@ -626,30 +560,28 @@ 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)]
#[serde(tag = "action", rename_all = "snake_case")] pub struct AgentSignalsTool {
pub enum AgentSignalsTool { /// Action to perform: 'broadcast' or 'query'.
/// Broadcast a signal to peer subagents. pub action: AgentSignalAction,
Broadcast { /// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). Required for broadcast; optional filter for query.
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). pub sender: Option<String>,
sender: String, /// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). Required for broadcast; optional filter for query.
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). pub signal_type: Option<String>,
signal_type: String, /// JSON or text payload containing event details or artifact URIs (required for action 'broadcast').
/// JSON or text payload containing event details or artifact URIs. pub payload: Option<String>,
payload: String, /// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour).
/// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour). pub ttl_seconds: Option<u64>,
ttl_seconds: Option<u64>, /// Optional limit on returned signals (for action 'query'). Defaults to 20.
}, 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)]
@@ -665,12 +597,21 @@ pub struct AutoSessionCheckpointTool {
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum HandoffMemoAction { pub enum HandoffMemoAction {
#[serde(alias = "leave", alias = "LEAVE", alias = "Leave")]
Leave, Leave,
#[serde(alias = "read", alias = "READ", alias = "Read")]
Read, Read,
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
Clear, Clear,
} }
/// 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'.
@@ -683,124 +624,150 @@ pub struct HandoffMemosTool {
pub ids: Option<Vec<String>>, pub ids: Option<Vec<String>>,
} }
/// Action-oriented task tracking system. #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "snake_case")]
#[serde(tag = "action", rename_all = "snake_case")] pub enum TaskAction {
pub enum TasksTool { #[serde(alias = "add", alias = "ADD", alias = "Add")]
/// Create a new task. Add,
Add { #[serde(alias = "update", alias = "UPDATE", alias = "Update")]
/// Task title. Update,
title: String, #[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
/// Task description. Delete,
description: Option<String>, #[serde(alias = "list", alias = "LIST", alias = "List")]
/// Optional parent task ID for subtasks. List,
parent_id: Option<String>, #[serde(
/// List of dependent task IDs. alias = "set_criteria",
dependencies: Option<Vec<String>>, alias = "setCriteria",
/// Branch associated with this task. alias = "SET_CRITERIA",
git_branch: Option<String>, alias = "SetCriteria"
/// Optional namespace filter or assignment. )]
namespace: Option<String>, SetCriteria,
/// Repository name. #[serde(alias = "verify", alias = "VERIFY", alias = "Verify")]
repo_name: Option<String>, Verify,
/// Priority: 'low', 'medium', 'high', 'critical'.
priority: Option<String>,
/// Assigned agent or subagent.
assigned_agent: Option<String>,
/// Command used to verify completion.
verification_command: Option<String>,
},
/// 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<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>,
}
} }
/// Project milestone management. /// 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)]
#[serde(tag = "action", rename_all = "snake_case")] pub struct TasksTool {
pub enum MilestonesTool { /// Action to perform: 'add', 'update', 'delete', 'list', 'set_criteria', or 'verify'.
/// Create a new milestone. pub action: TaskAction,
Add { /// Task ID (required for 'update', 'delete', 'set_criteria', 'verify').
/// Milestone title. #[serde(alias = "task_id", alias = "taskId")]
title: String, pub id: Option<String>,
/// Optional namespace filter or assignment. /// Task title (required for 'add').
namespace: Option<String>, #[serde(alias = "name")]
/// Optional target completion date. pub title: Option<String>,
target_date: Option<String>, /// Task description (optional for 'add').
/// Optional description of milestone scope. #[serde(alias = "desc")]
description: Option<String>, pub description: Option<String>,
/// Optional deliverables or task checklist. /// New status: 'pending', 'completed', or 'cancelled' (for 'update').
deliverables: Option<Vec<String>>, pub status: Option<String>,
/// Optional list of task IDs associated with this milestone. /// Parent task ID (optional for 'add').
#[serde(default)] #[serde(alias = "parentId", alias = "parent")]
task_ids: Option<Vec<String>>, pub parent_id: Option<String>,
/// Optional repository name. /// List of dependency task IDs (optional for 'add').
repo_name: Option<String>, #[serde(alias = "deps")]
}, pub dependencies: Option<Vec<String>>,
/// Update milestone status. /// Git branch filter or assignment.
Update { #[serde(alias = "branch")]
/// Milestone ID. pub git_branch: Option<String>,
id: String, /// Acceptance criteria (required for 'set_criteria').
/// New status. #[serde(alias = "acceptance_criteria", alias = "acceptanceCriteria")]
status: String, pub criteria: Option<Vec<String>>,
/// Optional deliverables or task checklist. /// Verification proof or details (optional for 'verify').
deliverables: Option<Vec<String>>, pub proof: Option<String>,
/// Optional list of task IDs associated with this milestone. /// Summary level: 'compact', 'detailed', or 'full' (for 'list').
#[serde(default)] pub summary_level: Option<String>,
task_ids: Option<Vec<String>>, /// Maximum tokens budget cap (for 'list').
}, pub max_tokens: Option<usize>,
/// List milestones. /// Optional namespace filter or assignment (defaults to 'default').
List { pub namespace: Option<String>,
/// Optional namespace. /// Optional repository name.
namespace: Option<String>, pub repo_name: Option<String>,
} /// Optional task priority: 'low', 'medium', 'high', 'urgent'.
pub priority: Option<String>,
/// Optional assigned subagent role or identifier.
pub assigned_agent: Option<String>,
/// Optional verification command to validate criteria completion.
/// Optional pagination offset for 'list'.
pub offset: Option<usize>,
pub verification_command: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MilestoneAction {
#[serde(alias = "add", alias = "ADD", alias = "Add")]
Add,
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
Update,
#[serde(alias = "list", alias = "LIST", alias = "List")]
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)]
pub struct MilestonesTool {
/// Action to perform: 'add', 'update', or 'list'.
pub action: MilestoneAction,
/// Milestone ID (required for 'update').
#[serde(alias = "milestone_id", alias = "milestoneId")]
pub id: Option<String>,
/// Milestone title (required for 'add').
#[serde(alias = "name")]
pub title: Option<String>,
/// New status (for 'update').
pub status: Option<String>,
/// Optional namespace filter or assignment.
pub namespace: Option<String>,
/// Optional target completion date.
pub target_date: Option<String>,
/// Optional description of milestone scope.
pub description: Option<String>,
/// Optional deliverables or task checklist.
pub deliverables: Option<Vec<String>>,
/// Optional list of task IDs associated with this milestone.
#[serde(default)]
pub task_ids: Option<Vec<String>>,
/// Optional repository name.
pub repo_name: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum SnippetAction { pub enum SnippetAction {
#[serde(alias = "store", alias = "STORE", alias = "Store")]
Store, Store,
#[serde(alias = "search", alias = "SEARCH", alias = "Search")]
Search, Search,
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
Delete, Delete,
#[serde(alias = "tag", alias = "TAG", alias = "Tag")]
Tag, Tag,
} }
/// 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'.
@@ -831,154 +798,177 @@ pub struct SnippetsTool {
pub line_range: Option<String>, pub line_range: Option<String>,
} }
/// Architectural Decision Records (ADRs) log. #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Clone)]
#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)] #[serde(rename_all = "snake_case")]
#[serde(tag = "action", rename_all = "snake_case")] pub enum DecisionAction {
pub enum DecisionsTool { #[serde(alias = "log", alias = "LOG", alias = "Log")]
/// Record an ADR. Log,
Log { #[serde(alias = "query", alias = "QUERY", alias = "Query")]
/// ADR title. Query,
title: String, #[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
/// ADR status. Delete,
status: Option<String>, #[serde(alias = "update", alias = "UPDATE", alias = "Update")]
/// Context & problem statement. Update,
context: Option<String>,
/// Decision made.
decision: Option<String>,
/// Consequences & tradeoffs.
consequences: 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>,
/// 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>,
},
/// Search ADRs.
Query {
/// Search query string.
query: Option<String>,
/// Optional namespace.
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,
}
} }
/// Technical debt backlog management. /// 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)]
pub struct DecisionsTool {
/// Action to perform: 'log', 'query', or 'delete'.
pub action: DecisionAction,
/// ADR ID (required for 'delete').
pub id: Option<String>,
/// ADR title (required for 'log').
pub title: Option<String>,
/// ADR status (for 'log').
pub status: Option<String>,
/// Context & problem statement (for 'log').
pub context: Option<String>,
/// Decision made (for 'log').
pub decision: Option<String>,
/// Consequences & tradeoffs (for 'log').
#[serde(alias = "consequence")]
pub consequences: Option<String>,
/// Search query string (for 'query').
pub query: Option<String>,
/// Optional namespace.
pub namespace: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
/// Optional author or architect behind the decision.
pub author: Option<String>,
/// Optional affected system components or crates.
pub affected_components: Option<Vec<String>>,
/// Optional alternative designs or libraries considered.
pub alternatives_considered: Option<Vec<String>>,
/// Optional superseded ADR ID.
pub supersedes: Option<String>,
/// Limit the number of query results. Defaults to 20.
pub limit: Option<usize>,
/// Include the full body of the matched decisions. Defaults to true.
pub include_body: Option<bool>,
/// Optional Git commit where decision was implemented.
pub git_commit: Option<String>,
/// Optional Git branch where decision was implemented.
pub git_branch: Option<String>,
/// Optional task ID linked to this decision.
pub task_id: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TechDebtAction {
#[serde(alias = "log", alias = "LOG", alias = "Log")]
Log,
#[serde(alias = "resolve", alias = "RESOLVE", alias = "Resolve")]
Resolve,
#[serde(alias = "list", alias = "LIST", alias = "List")]
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)]
#[serde(tag = "action", rename_all = "snake_case")] pub struct TechDebtTool {
pub enum TechDebtTool { /// Action to perform: 'log', 'resolve', or 'list'.
/// Record technical debt. pub action: TechDebtAction,
Log { /// Tech debt ID (required for 'resolve').
/// Tech debt description. #[serde(alias = "tech_debt_id", alias = "debt_id")]
description: String, pub id: Option<String>,
/// Optional title or summary of tech debt. /// Tech debt description (required for 'log').
title: Option<String>, #[serde(alias = "desc")]
/// Ideal solution. pub description: Option<String>,
ideal_solution: Option<String>, /// Optional title or summary of tech debt.
/// Associated git commit hash. pub title: Option<String>,
git_commit: Option<String>, /// Ideal solution (optional for 'log').
/// Associated git branch. #[serde(alias = "solution", alias = "fix")]
git_branch: Option<String>, pub ideal_solution: Option<String>,
/// Symbol references associated with the tech debt. /// Include resolved tech debt items (for 'list').
symbol_references: Option<Vec<String>>, pub include_resolved: Option<bool>,
/// Line range associated with the tech debt. /// Associated git commit hash.
line_range: Option<String>, #[serde(alias = "commit")]
/// Optional namespace. pub git_commit: Option<String>,
namespace: Option<String>, /// Associated git branch.
/// Optional debt severity: 'low', 'medium', 'high', 'critical'. #[serde(alias = "branch")]
severity: Option<String>, pub git_branch: Option<String>,
/// Optional repository name. /// Symbol references associated with the tech debt.
repo_name: Option<String>, pub symbol_references: Option<Vec<String>>,
/// Optional file path where debt exists. /// Line range associated with the tech debt.
file_path: Option<String>, pub line_range: Option<String>,
/// Optional workaround currently in place. /// Optional namespace.
workaround: Option<String>, pub namespace: Option<String>,
/// Optional effort estimate to fix. /// Optional debt severity: 'low', 'medium', 'high', 'critical'.
effort_estimate: Option<String>, pub severity: Option<String>,
}, /// Optional repository name.
/// Mark tech debt resolved. pub repo_name: Option<String>,
Resolve { /// Optional file path where debt exists.
/// Tech debt ID. pub file_path: Option<String>,
id: String, /// Optional workaround currently in place.
}, pub workaround: Option<String>,
/// List tech debt items. /// Optional effort estimate to fix.
List { pub effort_estimate: Option<String>,
/// Include resolved tech debt items. /// Summary level for list action: 'compact', 'detailed', or 'full'.
include_resolved: Option<bool>, pub summary_level: Option<String>,
/// Optional namespace. /// Optional token budget cap for list output.
namespace: Option<String>, pub max_tokens: Option<usize>,
/// 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)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum EnvAction { pub enum EnvAction {
#[serde(
alias = "update_fingerprint",
alias = "updateFingerprint",
alias = "UPDATE_FINGERPRINT",
alias = "UpdateFingerprint"
)]
UpdateFingerprint, UpdateFingerprint,
#[serde(
alias = "read_fingerprint",
alias = "readFingerprint",
alias = "READ_FINGERPRINT",
alias = "ReadFingerprint"
)]
ReadFingerprint, ReadFingerprint,
#[serde(
alias = "log_requirement",
alias = "logRequirement",
alias = "LOG_REQUIREMENT",
alias = "LogRequirement"
)]
LogRequirement, LogRequirement,
#[serde(alias = "register", alias = "REGISTER", alias = "Register")]
Register, Register,
#[serde(
alias = "get_details",
alias = "getDetails",
alias = "GET_DETAILS",
alias = "GetDetails"
)]
GetDetails, GetDetails,
} }
/// 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'.
@@ -1008,19 +998,37 @@ pub struct EnvironmentTool {
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ClipboardAction { pub enum ClipboardAction {
#[serde(
alias = "image",
alias = "IMAGE",
alias = "Image",
alias = "screenshot",
alias = "SCREENSHOT"
)]
Image, Image,
#[serde(alias = "text", alias = "TEXT", alias = "Text")]
Text, Text,
#[serde(alias = "history", alias = "HISTORY", alias = "History")]
History, History,
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
Clear, Clear,
#[serde(alias = "write", alias = "WRITE", alias = "Write")]
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)]
pub struct ClipboardTool { pub struct ClipboardTool {
/// Action to perform: 'image', 'text', 'history', 'clear', or 'write'. /// Action to perform: 'image', 'text', 'history', 'clear', 'read', or 'write'.
pub action: ClipboardAction, pub action: ClipboardAction,
/// Plain text content (for 'write'). /// Plain text content (for 'write').
pub text: Option<String>, pub text: Option<String>,
+5 -4
View File
@@ -29,10 +29,11 @@ fn test_eager_tools_parity() {
let nvim_source = let nvim_source =
std::fs::read_to_string("../nvim-core/src/lib.rs").expect("Failed to read nvim lib.rs"); std::fs::read_to_string("../nvim-core/src/lib.rs").expect("Failed to read nvim lib.rs");
let mut nvim_tools = HashSet::new(); let mut nvim_tools = HashSet::new();
let parts: Vec<&str> = nvim_source.split("tool_def").collect(); for line in nvim_source.lines() {
for part in parts.iter().skip(1) { if line.contains("\"name\": \"nvim_")
if let Some(start) = part.find("(\"") { && let Some(start) = line.find("\"name\": \"")
let rest = &part[start + 2..]; {
let rest = &line[start + 9..];
if let Some(end) = rest.find('"') { if let Some(end) = rest.find('"') {
nvim_tools.insert(rest[..end].to_string()); nvim_tools.insert(rest[..end].to_string());
} }
View File
Whitespace-only changes.