style: format seluruh workspace dengan cargo fmt
Menyeragamkan format kode sesuai rustfmt (126 file). Sebelumnya lefthook pre-commit 'cargo fmt --check' akan gagal pada commit apa pun.
This commit is contained in:
@@ -131,7 +131,8 @@ impl Tool for BestPractice {
|
||||
}
|
||||
// Suggest a template.
|
||||
if let Some(parsed) = eng.parse_commit(&msg) {
|
||||
let tpl = eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref());
|
||||
let tpl =
|
||||
eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref());
|
||||
out.push_str(&format!("\nTemplate: {tpl}\n"));
|
||||
}
|
||||
Ok(out)
|
||||
@@ -309,8 +310,14 @@ mod tests {
|
||||
let tool = BestPractice;
|
||||
let args = json!({"action": "list_skills"});
|
||||
let result = tool.run(&test_ctx(), &args).unwrap();
|
||||
assert!(result.contains("clean-code"), "should list clean-code: {result}");
|
||||
assert!(result.contains("commit-convention"), "should list commit-convention: {result}");
|
||||
assert!(
|
||||
result.contains("clean-code"),
|
||||
"should list clean-code: {result}"
|
||||
);
|
||||
assert!(
|
||||
result.contains("commit-convention"),
|
||||
"should list commit-convention: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -318,7 +325,10 @@ mod tests {
|
||||
let tool = BestPractice;
|
||||
let args = json!({"action": "get_skill", "skill_name": "clean-code"});
|
||||
let result = tool.run(&test_ctx(), &args).unwrap();
|
||||
assert!(result.contains("Clean Code"), "should contain skill content: {result}");
|
||||
assert!(
|
||||
result.contains("Clean Code"),
|
||||
"should contain skill content: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -326,7 +336,10 @@ mod tests {
|
||||
let tool = CommitConvention;
|
||||
let args = json!({"message": "feat(tool): add best practice audit"});
|
||||
let result = tool.run(&test_ctx(), &args).unwrap();
|
||||
assert!(result.contains("✅"), "valid commit should succeed: {result}");
|
||||
assert!(
|
||||
result.contains("✅"),
|
||||
"valid commit should succeed: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -334,6 +347,9 @@ mod tests {
|
||||
let tool = CommitConvention;
|
||||
let args = json!({"message": "Add new feature"});
|
||||
let result = tool.run(&test_ctx(), &args).unwrap();
|
||||
assert!(result.contains("❌"), "invalid commit should fail: {result}");
|
||||
assert!(
|
||||
result.contains("❌"),
|
||||
"invalid commit should fail: {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ pub struct ToolCtx {
|
||||
pub origin: crate::Origin,
|
||||
pub graduated_checks: Vec<crate::tools::GraduatedCheck>,
|
||||
pub lsp_manager: Arc<Mutex<crate::lsp::manager::LspManager>>,
|
||||
pub turn_events:
|
||||
Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
|
||||
pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
|
||||
pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>,
|
||||
pub abort_flag: Option<Arc<AtomicBool>>,
|
||||
}
|
||||
@@ -41,8 +40,7 @@ pub struct ToolCtxBuilder {
|
||||
pub origin: crate::Origin,
|
||||
pub graduated_checks: Vec<crate::tools::GraduatedCheck>,
|
||||
pub lsp_manager: Arc<Mutex<crate::lsp::manager::LspManager>>,
|
||||
pub turn_events:
|
||||
Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
|
||||
pub turn_events: Option<Arc<Mutex<std::collections::VecDeque<crate::TurnEvent>>>>,
|
||||
pub workflow_findings: Option<Arc<Mutex<Vec<String>>>>,
|
||||
pub abort_flag: Option<Arc<AtomicBool>>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::future::Future;
|
||||
use anyhow::Result;
|
||||
use zesdex_application::agent::ToolExecutor;
|
||||
use crate::tools::{all_tools, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use std::future::Future;
|
||||
use zesdex_application::agent::ToolExecutor;
|
||||
|
||||
pub struct InfrastructureToolExecutor {
|
||||
ctx: ToolCtx,
|
||||
@@ -27,14 +27,10 @@ impl ToolExecutor for InfrastructureToolExecutor {
|
||||
let tool_opt = self.tools.iter().find(|t| t.name() == tool_name);
|
||||
let ctx = self.ctx.clone();
|
||||
let args = args.clone();
|
||||
|
||||
|
||||
async move {
|
||||
match tool_opt {
|
||||
Some(tool) => {
|
||||
tokio::task::block_in_place(move || {
|
||||
tool.run(&ctx, &args)
|
||||
})
|
||||
}
|
||||
Some(tool) => tokio::task::block_in_place(move || tool.run(&ctx, &args)),
|
||||
None => {
|
||||
anyhow::bail!("Unknown tool: {}", tool_name)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::tools::shell_filter::git::check_git_destructive;
|
||||
use crate::tools::{execute_cmd, Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, warn, instrument};
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
/// Tool that executes safe git operations (status, log, diff, commit, branch, etc.).
|
||||
///
|
||||
|
||||
@@ -56,15 +56,13 @@ impl Tool for GitWorktree {
|
||||
let branch = crate::tools::arg_str(args, "branch")?;
|
||||
info!(path, branch, "adding worktree");
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git")
|
||||
.args(["worktree", "add", &path, &branch]),
|
||||
std::process::Command::new("git").args(["worktree", "add", &path, &branch]),
|
||||
)?;
|
||||
Ok(output)
|
||||
}
|
||||
"list" => {
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git").args(["worktree", "list"]),
|
||||
)?;
|
||||
let output =
|
||||
execute_cmd(std::process::Command::new("git").args(["worktree", "list"]))?;
|
||||
Ok(output)
|
||||
}
|
||||
"remove" => {
|
||||
@@ -77,9 +75,8 @@ impl Tool for GitWorktree {
|
||||
}
|
||||
"prune" => {
|
||||
info!("pruning stale worktree metadata");
|
||||
let output = execute_cmd(
|
||||
std::process::Command::new("git").args(["worktree", "prune"]),
|
||||
)?;
|
||||
let output =
|
||||
execute_cmd(std::process::Command::new("git").args(["worktree", "prune"]))?;
|
||||
Ok(output)
|
||||
}
|
||||
_ => anyhow::bail!("unknown action: {}", action),
|
||||
|
||||
@@ -11,11 +11,7 @@ pub struct GraduatedCheck {
|
||||
}
|
||||
|
||||
/// Check which graduated checks apply to a given file path/content pair.
|
||||
pub fn check_graduated_checks(
|
||||
path: &str,
|
||||
content: &str,
|
||||
checks: &[GraduatedCheck],
|
||||
) -> Vec<String> {
|
||||
pub fn check_graduated_checks(path: &str, content: &str, checks: &[GraduatedCheck]) -> Vec<String> {
|
||||
let mut matches = Vec::new();
|
||||
for check in checks {
|
||||
if path.contains(&check.pattern) || content.contains(&check.rule) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that requests code completion suggestions from an LSP server.
|
||||
///
|
||||
@@ -65,10 +65,13 @@ impl Tool for LspCompletion {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/completion", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/completion",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that connects to an LSP language server for a given language.
|
||||
///
|
||||
@@ -52,7 +52,11 @@ impl Tool for LspConnect {
|
||||
let extra_args: Vec<String> = args
|
||||
.get("args")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
info!(language, command, extra_args = ?extra_args, "LSP connect requested");
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that resolves a symbol's definition location via LSP.
|
||||
///
|
||||
@@ -66,10 +66,13 @@ impl Tool for LspDefinition {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/definition", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/definition",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that retrieves diagnostics (errors, warnings) from the LSP for a file.
|
||||
///
|
||||
@@ -56,9 +56,12 @@ impl Tool for LspDiagnostics {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/diagnostic", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/diagnostic",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that disconnects an LSP language server for a given language.
|
||||
///
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that retrieves hover information for a symbol at a position via LSP.
|
||||
///
|
||||
@@ -66,10 +66,13 @@ impl Tool for LspHover {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/hover", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/hover",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -9,10 +9,10 @@ pub mod disconnect;
|
||||
pub mod hover;
|
||||
pub mod references;
|
||||
|
||||
pub use connect::LspConnect;
|
||||
pub use diagnostics::LspDiagnostics;
|
||||
pub use hover::LspHover;
|
||||
pub use completion::LspCompletion;
|
||||
pub use connect::LspConnect;
|
||||
pub use definition::LspDefinition;
|
||||
pub use references::LspReferences;
|
||||
pub use diagnostics::LspDiagnostics;
|
||||
pub use disconnect::LspDisconnect;
|
||||
pub use hover::LspHover;
|
||||
pub use references::LspReferences;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::tools::{Tool, ToolCtx};
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{info, error, instrument};
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
/// Tool that finds all references to a symbol at a position via LSP.
|
||||
///
|
||||
@@ -66,10 +66,13 @@ impl Tool for LspReferences {
|
||||
}
|
||||
};
|
||||
if let Some(client) = manager.get_client(&language) {
|
||||
let result = client.send_request("textDocument/references", &json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}))?;
|
||||
let result = client.send_request(
|
||||
"textDocument/references",
|
||||
&json!({
|
||||
"textDocument": { "uri": format!("file://{}", path) },
|
||||
"position": { "line": line, "character": character }
|
||||
}),
|
||||
)?;
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
} else {
|
||||
anyhow::bail!("no LSP client connected for '{language}'")
|
||||
|
||||
@@ -52,8 +52,8 @@ pub mod sequential_think;
|
||||
pub mod shell;
|
||||
pub mod shell_filter;
|
||||
pub mod spawn;
|
||||
pub mod utility;
|
||||
pub mod util;
|
||||
pub mod utility;
|
||||
pub mod web_search;
|
||||
pub mod workflow;
|
||||
|
||||
|
||||
@@ -112,10 +112,7 @@ impl Tool for ParallelDelegate {
|
||||
dirs.iter()
|
||||
.filter_map(|d| {
|
||||
let directive = d.get("directive").and_then(|v| v.as_str())?;
|
||||
let access_str = d
|
||||
.get("access")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("write");
|
||||
let access_str = d.get("access").and_then(|v| v.as_str()).unwrap_or("write");
|
||||
let access = match access_str {
|
||||
"read" => AccessTier::Read,
|
||||
"full" => AccessTier::Full,
|
||||
@@ -162,12 +159,7 @@ impl Tool for ParallelDelegate {
|
||||
);
|
||||
|
||||
debug!(agent_index = i, access = ?access, "spawning parallel agent");
|
||||
let handle = spawn_subagent(
|
||||
subagent_ctx,
|
||||
directive.clone(),
|
||||
*access,
|
||||
ctx.clone(),
|
||||
);
|
||||
let handle = spawn_subagent(subagent_ctx, directive.clone(), *access, ctx.clone());
|
||||
handles.push((i, handle));
|
||||
}
|
||||
|
||||
@@ -181,11 +173,7 @@ impl Tool for ParallelDelegate {
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!(agent_index = i, error = %e, "parallel agent failed");
|
||||
results.push((
|
||||
i,
|
||||
directives[i].0.clone(),
|
||||
format!("[ERROR] {e}"),
|
||||
));
|
||||
results.push((i, directives[i].0.clone(), format!("[ERROR] {e}")));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(agent_index = i, error = ?e, "parallel agent panicked");
|
||||
@@ -201,7 +189,8 @@ impl Tool for ParallelDelegate {
|
||||
// Consolidate results
|
||||
if synthesize && results.len() > 1 {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
let consolidated = rt.block_on(consolidate_results(&results, &base_url, &api_key, &model))?;
|
||||
let consolidated =
|
||||
rt.block_on(consolidate_results(&results, &base_url, &api_key, &model))?;
|
||||
Ok(format!(
|
||||
"## Parallel Delegation Complete\n\n**Task:** {task}\n**Parallel agents:** {}\n\n{}",
|
||||
results.len(),
|
||||
@@ -213,7 +202,10 @@ impl Tool for ParallelDelegate {
|
||||
results.len()
|
||||
);
|
||||
for (i, directive, result) in &results {
|
||||
output.push_str(&format!("---\n### Agent {}: {}\n\n{}\n", i, directive, result));
|
||||
output.push_str(&format!(
|
||||
"---\n### Agent {}: {}\n\n{}\n",
|
||||
i, directive, result
|
||||
));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
@@ -234,9 +226,8 @@ async fn auto_split_task(
|
||||
Some(base_url.to_string()),
|
||||
);
|
||||
|
||||
let sys_msg = zesdex_domain::core::ChatMessage::system(
|
||||
format!(
|
||||
"You are a task decomposition expert. Split the following task into {max_parallel} \
|
||||
let sys_msg = zesdex_domain::core::ChatMessage::system(format!(
|
||||
"You are a task decomposition expert. Split the following task into {max_parallel} \
|
||||
independent sub-tasks that can run in parallel. Each sub-task must be self-contained \
|
||||
and produce useful output independently.\n\n\
|
||||
Output your response as a JSON array of objects, each with:\n\
|
||||
@@ -244,15 +235,16 @@ async fn auto_split_task(
|
||||
- \"access\": one of \"read\", \"write\", or \"full\"\n\n\
|
||||
IMPORTANT: Return ONLY valid JSON, no other text. Example:\n\
|
||||
[{{\"directive\": \"Create the User model with fields...\", \"access\": \"write\"}}]"
|
||||
)
|
||||
);
|
||||
));
|
||||
|
||||
let user_msg =
|
||||
zesdex_domain::core::ChatMessage::user(format!("Task: {task}\n\nSplit into {max_parallel} parallel directives:"));
|
||||
let user_msg = zesdex_domain::core::ChatMessage::user(format!(
|
||||
"Task: {task}\n\nSplit into {max_parallel} parallel directives:"
|
||||
));
|
||||
|
||||
use zesdex_application::ports::ProviderService;
|
||||
match client
|
||||
.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.4)).await
|
||||
.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.4))
|
||||
.await
|
||||
{
|
||||
Ok((response, _)) => {
|
||||
let text = response.content.unwrap_or_default();
|
||||
@@ -320,10 +312,7 @@ fn fallback_split(task: &str, max_parallel: usize) -> Vec<(String, AccessTier)>
|
||||
}
|
||||
|
||||
if task.contains("test") || task.contains("unit") {
|
||||
directives.push((
|
||||
format!("Write unit tests for: {task}"),
|
||||
AccessTier::Read,
|
||||
));
|
||||
directives.push((format!("Write unit tests for: {task}"), AccessTier::Read));
|
||||
}
|
||||
|
||||
if directives.is_empty() {
|
||||
@@ -370,7 +359,10 @@ async fn consolidate_results(
|
||||
));
|
||||
|
||||
use zesdex_application::ports::ProviderService;
|
||||
match client.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.3)).await {
|
||||
match client
|
||||
.chat(&[sys_msg, user_msg], None, Some(2048), Some(0.3))
|
||||
.await
|
||||
{
|
||||
Ok((response, _)) => Ok(response.content.unwrap_or_else(|| summary.clone())),
|
||||
Err(e) => {
|
||||
warn!(error = %e, "consolidation LLM call failed, using raw concatenation");
|
||||
|
||||
@@ -158,11 +158,7 @@ impl Tool for Glob {
|
||||
let p = entry.path();
|
||||
if glob_set.is_match(p) {
|
||||
let rel_path = p.strip_prefix(&root).unwrap_or(p).display().to_string();
|
||||
matches.push(format!(
|
||||
"{}{}",
|
||||
rel_path,
|
||||
if p.is_dir() { "/" } else { "" }
|
||||
));
|
||||
matches.push(format!("{}{}", rel_path, if p.is_dir() { "/" } else { "" }));
|
||||
}
|
||||
}
|
||||
matches.sort();
|
||||
|
||||
@@ -304,10 +304,7 @@ impl SymbolIndex {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ext = file_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("");
|
||||
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
let extractor = match ext_dispatch.get(ext) {
|
||||
Some(f) => f,
|
||||
_ => continue, // unsupported extension
|
||||
@@ -704,8 +701,8 @@ fn extract_typescript(content: &str, rel_path: &str) -> Vec<CodeSymbol> {
|
||||
if let Some(caps) = r.ts_var_export.captures(trimmed) {
|
||||
let name = caps.get(1).unwrap().as_str().to_string();
|
||||
// Only capture top-level (indentation 0) or exported
|
||||
let is_top_level = line.starts_with(|c: char| !c.is_whitespace())
|
||||
|| trimmed.starts_with("export");
|
||||
let is_top_level =
|
||||
line.starts_with(|c: char| !c.is_whitespace()) || trimmed.starts_with("export");
|
||||
if is_top_level {
|
||||
let kind = if trimmed.contains("const ") {
|
||||
SymbolKind::Constant
|
||||
@@ -963,10 +960,7 @@ fn extract_python(content: &str, rel_path: &str) -> Vec<CodeSymbol> {
|
||||
&& !trimmed.contains("==");
|
||||
if is_top_level {
|
||||
let name = trimmed.split('=').next().unwrap_or("").trim().to_string();
|
||||
if !name.is_empty()
|
||||
&& !name.starts_with('_')
|
||||
&& !name.contains(' ')
|
||||
{
|
||||
if !name.is_empty() && !name.starts_with('_') && !name.contains(' ') {
|
||||
let kind = if name.chars().all(|c| c.is_uppercase() || c == '_') {
|
||||
SymbolKind::Constant
|
||||
} else {
|
||||
@@ -1157,10 +1151,7 @@ pub fn format_symbol_listing(index: &SymbolIndex) -> String {
|
||||
std::collections::BTreeMap::new();
|
||||
for sym in syms {
|
||||
let kind_str = sym.kind.to_string();
|
||||
by_kind
|
||||
.entry(kind_str)
|
||||
.or_default()
|
||||
.push(sym.name.as_str());
|
||||
by_kind.entry(kind_str).or_default().push(sym.name.as_str());
|
||||
}
|
||||
for (kind, names) in &by_kind {
|
||||
out.push_str(&format!(" {kind}: {}\n", names.join(", ")));
|
||||
@@ -1312,7 +1303,11 @@ impl Tool for SemanticSearch {
|
||||
}
|
||||
|
||||
let total = index.len();
|
||||
info!(matched = filtered.len(), total_indexed = total, "semantic search completed");
|
||||
info!(
|
||||
matched = filtered.len(),
|
||||
total_indexed = total,
|
||||
"semantic search completed"
|
||||
);
|
||||
|
||||
let mut by_file: std::collections::BTreeMap<String, Vec<&&CodeSymbol>> =
|
||||
std::collections::BTreeMap::new();
|
||||
@@ -1350,7 +1345,11 @@ impl Tool for SemanticSearch {
|
||||
sym.line,
|
||||
sym.context.trim(),
|
||||
doc_str,
|
||||
if sym.context.trim().len() > 80 { "…" } else { "" }
|
||||
if sym.context.trim().len() > 80 {
|
||||
"…"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
));
|
||||
}
|
||||
output.push('\n');
|
||||
@@ -1574,12 +1573,7 @@ impl Tool for ListSymbols {
|
||||
for (file, file_syms) in &by_file {
|
||||
out.push_str(&format!("`{file}`:\n"));
|
||||
for sym in file_syms {
|
||||
out.push_str(&format!(
|
||||
" `{}` {} L{}\n",
|
||||
sym.kind,
|
||||
sym.name,
|
||||
sym.line,
|
||||
));
|
||||
out.push_str(&format!(" `{}` {} L{}\n", sym.kind, sym.name, sym.line,));
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
@@ -1633,51 +1627,82 @@ mod tests {
|
||||
fn test_extract_typescript_function_and_class() {
|
||||
let content = "function hello() {}\nexport class User {}\ninterface Person {}\n";
|
||||
let symbols = extract_typescript(content, "test.ts");
|
||||
assert!(symbols.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols.iter().any(|s| s.name == "User" && s.kind == SymbolKind::Class));
|
||||
assert!(symbols.iter().any(|s| s.name == "Person" && s.kind == SymbolKind::Interface));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "hello" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "User" && s.kind == SymbolKind::Class));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "Person" && s.kind == SymbolKind::Interface));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_typescript_const() {
|
||||
let content = "export const API_URL = 'http://example.com';\nconst MAX_RETRIES = 3;\n";
|
||||
let symbols = extract_typescript(content, "test.ts");
|
||||
assert!(symbols.iter().any(|s| s.name == "API_URL" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols.iter().any(|s| s.name == "MAX_RETRIES" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "API_URL" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "MAX_RETRIES" && s.kind == SymbolKind::Constant));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_python_def_and_class() {
|
||||
let content = "class MyClass:\n def method(self):\n pass\n\ndef top_func():\n pass\n";
|
||||
let content =
|
||||
"class MyClass:\n def method(self):\n pass\n\ndef top_func():\n pass\n";
|
||||
let symbols = extract_python(content, "test.py");
|
||||
assert!(symbols.iter().any(|s| s.name == "MyClass" && s.kind == SymbolKind::Class));
|
||||
assert!(symbols.iter().any(|s| s.name == "MyClass.method" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols.iter().any(|s| s.name == "top_func" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "MyClass" && s.kind == SymbolKind::Class));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "MyClass.method" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "top_func" && s.kind == SymbolKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_python_variable() {
|
||||
let content = "DATABASE_URL = 'postgres://localhost'\nconfig_path = '/etc/app'\n";
|
||||
let symbols = extract_python(content, "test.py");
|
||||
assert!(symbols.iter().any(|s| s.name == "DATABASE_URL" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols.iter().any(|s| s.name == "config_path" && s.kind == SymbolKind::Variable));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "DATABASE_URL" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "config_path" && s.kind == SymbolKind::Variable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_go_func_and_struct() {
|
||||
let content = "func main() {}\nfunc (s *Server) Serve() {}\ntype Config struct {\n Name string\n}\n";
|
||||
let symbols = extract_go(content, "test.go");
|
||||
assert!(symbols.iter().any(|s| s.name == "main" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols.iter().any(|s| s.name == "Serve" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols.iter().any(|s| s.name == "Config" && s.kind == SymbolKind::Struct));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "main" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "Serve" && s.kind == SymbolKind::Function));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "Config" && s.kind == SymbolKind::Struct));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_go_const_and_var() {
|
||||
let content = "const VERSION = \"1.0\"\nvar DefaultPort = 8080\n";
|
||||
let symbols = extract_go(content, "test.go");
|
||||
assert!(symbols.iter().any(|s| s.name == "VERSION" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols.iter().any(|s| s.name == "DefaultPort" && s.kind == SymbolKind::Variable));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "VERSION" && s.kind == SymbolKind::Constant));
|
||||
assert!(symbols
|
||||
.iter()
|
||||
.any(|s| s.name == "DefaultPort" && s.kind == SymbolKind::Variable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -89,9 +89,13 @@ impl Tool for Bash {
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = Duration::from_millis(timeout_ms);
|
||||
|
||||
let mut child_stdout = child.stdout.take()
|
||||
let mut child_stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to capture stdout"))?;
|
||||
let mut child_stderr = child.stderr.take()
|
||||
let mut child_stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to capture stderr"))?;
|
||||
|
||||
let stdout_handle = std::thread::spawn(move || -> std::io::Result<Vec<u8>> {
|
||||
|
||||
@@ -33,8 +33,10 @@ pub fn is_credential_path(path: &str) -> bool {
|
||||
/// return list of suspected credential reads.
|
||||
#[instrument(skip(cmd))]
|
||||
pub fn check_credential_read(cmd: &str) -> Vec<String> {
|
||||
let re = Regex::new(r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#)
|
||||
.expect("hardcoded credential-read regex is valid");
|
||||
let re = Regex::new(
|
||||
r#"(?i)(?:cat|head|tail|less|more|vim?|nano|xdg-open|open|type|echo)\s+(~?/[\w/.@-]+)"#,
|
||||
)
|
||||
.expect("hardcoded credential-read regex is valid");
|
||||
let mut findings = Vec::new();
|
||||
for cap in re.captures_iter(cmd) {
|
||||
let path = cap.get(1).map(|m| m.as_str()).unwrap_or("");
|
||||
|
||||
@@ -28,10 +28,7 @@ pub fn check_git_destructive(cmd: &str) -> Result<(), String> {
|
||||
|
||||
for pattern in &destructive_patterns {
|
||||
if cmd_lower.contains(pattern) {
|
||||
return Err(format!(
|
||||
"destructive git operation blocked: '{}'",
|
||||
pattern
|
||||
));
|
||||
return Err(format!("destructive git operation blocked: '{}'", pattern));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,7 @@ pub fn execute_cmd(cmd: &mut std::process::Command) -> Result<String> {
|
||||
let combined = if stderr.is_empty() {
|
||||
stdout
|
||||
} else {
|
||||
format!("{}\n{}", stdout, stderr)
|
||||
.trim()
|
||||
.to_string()
|
||||
format!("{}\n{}", stdout, stderr).trim().to_string()
|
||||
};
|
||||
let code = output.status.code().unwrap_or(-1);
|
||||
if output.status.success() {
|
||||
@@ -151,6 +149,10 @@ pub fn log_write_edit_tool(
|
||||
let _ = repo.append(session_dir, &mut el, entry);
|
||||
debug!(tool = tool_name, path = path, "edit-log entry persisted");
|
||||
} else {
|
||||
warn!(tool = tool_name, path = path, "failed to open edit-log repository");
|
||||
warn!(
|
||||
tool = tool_name,
|
||||
path = path,
|
||||
"failed to open edit-log repository"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,28 +42,33 @@ impl Tool for Todofinish {
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let item = crate::tools::arg_str(args, "item")?;
|
||||
info!(item, "todofinish invoked");
|
||||
|
||||
|
||||
let todo_path = ctx.session_dir.join("TODO.md");
|
||||
let mut content = std::fs::read_to_string(&todo_path).unwrap_or_default();
|
||||
|
||||
|
||||
// Very basic replace to mark as finished
|
||||
let mut replaced = false;
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let mut new_lines = Vec::new();
|
||||
for line in lines {
|
||||
if line.contains(&item) && line.starts_with("- [") {
|
||||
let s = line.replacen("- [high]", "- [x]", 1)
|
||||
.replacen("- [medium]", "- [x]", 1)
|
||||
.replacen("- [low]", "- [x]", 1);
|
||||
let s = line
|
||||
.replacen("- [high]", "- [x]", 1)
|
||||
.replacen("- [medium]", "- [x]", 1)
|
||||
.replacen("- [low]", "- [x]", 1);
|
||||
// if it didn't match those, just replace the first `[`
|
||||
let s = if s == line { line.replacen("[ ]", "[x]", 1) } else { s };
|
||||
let s = if s == line {
|
||||
line.replacen("[ ]", "[x]", 1)
|
||||
} else {
|
||||
s
|
||||
};
|
||||
new_lines.push(s);
|
||||
replaced = true;
|
||||
} else {
|
||||
new_lines.push(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if replaced {
|
||||
content = new_lines.join("\n") + "\n";
|
||||
let _ = std::fs::write(&todo_path, &content);
|
||||
@@ -76,7 +81,7 @@ impl Tool for Todofinish {
|
||||
} else {
|
||||
info!(item, "TODO item not found in TODO.md — nothing to mark");
|
||||
}
|
||||
|
||||
|
||||
Ok(format!("TODO completed: {}", item))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ impl Tool for Todowrite {
|
||||
info!(item, priority, "todowrite invoked");
|
||||
|
||||
let todo_line = format!("- [{}] {}\n", priority, item);
|
||||
|
||||
|
||||
let todo_path = ctx.session_dir.join("TODO.md");
|
||||
let mut content = std::fs::read_to_string(&todo_path).unwrap_or_default();
|
||||
content.push_str(&todo_line);
|
||||
|
||||
|
||||
let _ = std::fs::write(&todo_path, &content);
|
||||
|
||||
if let Some(events) = &ctx.turn_events {
|
||||
|
||||
@@ -75,8 +75,8 @@ impl Tool for WebSearch {
|
||||
|
||||
info!(query = %query, max_results, fetch_content, categories = %categories, "web search starting");
|
||||
|
||||
let searxng_url = std::env::var("SEARXNG_URL")
|
||||
.unwrap_or_else(|_| DEFAULT_SEARXNG_URL.to_string());
|
||||
let searxng_url =
|
||||
std::env::var("SEARXNG_URL").unwrap_or_else(|_| DEFAULT_SEARXNG_URL.to_string());
|
||||
|
||||
// Build the SearXNG JSON search URL
|
||||
let search_url = format!(
|
||||
@@ -138,10 +138,7 @@ impl Tool for WebSearch {
|
||||
.get("title")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Untitled");
|
||||
let url = result
|
||||
.get("url")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let url = result.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let snippet = result
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -176,7 +173,9 @@ impl Tool for WebSearch {
|
||||
|
||||
// Add suggestion for more specific queries if very few results
|
||||
if result_count < 3 {
|
||||
output.push_str("---\n*Few results. Try a more specific query or different categories.*\n");
|
||||
output.push_str(
|
||||
"---\n*Few results. Try a more specific query or different categories.*\n",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
@@ -262,10 +261,7 @@ fn fetch_page_content(url: &str, client: &reqwest::blocking::Client) -> Result<S
|
||||
}
|
||||
|
||||
// Clean up whitespace
|
||||
let cleaned_text = text
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let cleaned_text = text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
|
||||
if cleaned_text.is_empty() {
|
||||
anyhow::bail!("no readable content found on page");
|
||||
@@ -276,8 +272,5 @@ fn fetch_page_content(url: &str, client: &reqwest::blocking::Client) -> Result<S
|
||||
|
||||
/// Limit a string to at most `max_lines` lines.
|
||||
fn limit_lines(s: &str, max_lines: usize) -> String {
|
||||
s.lines()
|
||||
.take(max_lines)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
s.lines().take(max_lines).collect::<Vec<_>>().join("\n")
|
||||
}
|
||||
|
||||
@@ -239,13 +239,8 @@ impl Tool for HiveMind {
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|d| {
|
||||
let directive = d
|
||||
.get("directive")
|
||||
.and_then(|v| v.as_str())?;
|
||||
let access = d
|
||||
.get("access")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("read");
|
||||
let directive = d.get("directive").and_then(|v| v.as_str())?;
|
||||
let access = d.get("access").and_then(|v| v.as_str()).unwrap_or("read");
|
||||
Some(NodeDirective {
|
||||
directive: directive.to_string(),
|
||||
access_tier: access.to_string(),
|
||||
@@ -255,14 +250,17 @@ impl Tool for HiveMind {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
info!(cycle_index = cycle_idx, node_count = directives.len(), "executing hive-mind cycle");
|
||||
info!(
|
||||
cycle_index = cycle_idx,
|
||||
node_count = directives.len(),
|
||||
"executing hive-mind cycle"
|
||||
);
|
||||
let cycle = CognitiveCycle {
|
||||
index: cycle_idx as u32,
|
||||
directives,
|
||||
};
|
||||
|
||||
let nodes: Vec<NodeOutput> =
|
||||
rt.block_on(async { execute_cycle(&cycle, ctx).await })?;
|
||||
let nodes: Vec<NodeOutput> = rt.block_on(async { execute_cycle(&cycle, ctx).await })?;
|
||||
all_node_outputs.extend(nodes);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user