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:
asepharyana
2026-08-27 22:10:28 +07:00
parent 884b19ccb5
commit 7b0b53671f
127 changed files with 1271 additions and 1156 deletions
+1 -4
View File
@@ -16,10 +16,7 @@ pub trait ToolExecutor: Send + Sync {
/// Service for running agent turns asynchronously.
pub trait AgentTurnService: Send + Sync {
/// Run a full agent turn loop asynchronously.
fn run_turn(
&self,
params: AgentTurnParams,
) -> impl Future<Output = Result<()>> + Send;
fn run_turn(&self, params: AgentTurnParams) -> impl Future<Output = Result<()>> + Send;
}
pub mod explore;
+18 -23
View File
@@ -7,8 +7,8 @@ use zesdex_domain::agent::{AgentTurnParams, TurnEvent};
use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
use zesdex_domain::main_agent_prompt;
use crate::ports::ProviderService;
use super::{ExploreService, ToolExecutor};
use crate::ports::ProviderService;
/// Maximum tool-call iterations per agent turn before forcing termination.
const MAX_TURN_ITERATIONS: u32 = 50;
@@ -124,11 +124,7 @@ pub struct AgentTurnServiceImpl<P: ProviderService, T: ToolExecutor> {
}
impl<P: ProviderService, T: ToolExecutor> AgentTurnServiceImpl<P, T> {
pub fn new(
provider: Arc<P>,
tool_executor: Arc<T>,
tool_defs: Vec<ToolDef>,
) -> Self {
pub fn new(provider: Arc<P>, tool_executor: Arc<T>, tool_defs: Vec<ToolDef>) -> Self {
Self {
provider,
tool_executor,
@@ -203,7 +199,10 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
},
);
match explorer.explore(&user_query, &workspace_root, &params.turn_events).await {
match explorer
.explore(&user_query, &workspace_root, &params.turn_events)
.await
{
Ok(output) => {
// Insert each context message as a system message.
// They go at index 0 and are removed after the turn
@@ -236,7 +235,9 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
// entire turn, avoiding per-iteration clones of the full message list.
// It is removed before emitting the Compacted event so persistence
// does not store the prompt redundantly.
params.messages.insert(0, ChatMessage::system(main_agent_prompt()));
params
.messages
.insert(0, ChatMessage::system(main_agent_prompt()));
let original_count = params.messages.len();
for iteration in 0..MAX_TURN_ITERATIONS {
@@ -287,7 +288,8 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
// ── Execute each tool call ──────────────────────────
for tc in &tool_calls {
let output =
execute_tool_call(self.tool_executor.as_ref(), &params.turn_events, tc).await;
execute_tool_call(self.tool_executor.as_ref(), &params.turn_events, tc)
.await;
params
.messages
.push(ChatMessage::tool(tc.id.clone(), output));
@@ -295,10 +297,7 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
}
Err(e) => {
warn!("{e}");
push_event(
&params.turn_events,
TurnEvent::Error(e),
);
push_event(&params.turn_events, TurnEvent::Error(e));
break;
}
}
@@ -307,10 +306,7 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
// Remove the synthetic sys_msg before shipping events to the TUI
// so the transcript shows only the actual user/assistant/tool exchange.
let compacted: Vec<ChatMessage> = params.messages.drain(original_count - 1..).collect();
push_event(
&params.turn_events,
TurnEvent::Compacted(compacted),
);
push_event(&params.turn_events, TurnEvent::Compacted(compacted));
push_event(&params.turn_events, TurnEvent::Done);
params.in_flight.store(false, Ordering::SeqCst);
@@ -341,15 +337,16 @@ pub async fn compact_messages_with_ai<P: ProviderService>(
let split_idx = messages.len() - COMPACT_KEEP_TAIL;
let evicted: Vec<_> = messages.drain(..split_idx).collect();
let mut summary_prompt = vec![
ChatMessage::system(zesdex_domain::compaction_prompt()),
];
let mut summary_prompt = vec![ChatMessage::system(zesdex_domain::compaction_prompt())];
summary_prompt.extend(evicted);
summary_prompt.push(ChatMessage::user(
"Please summarise our previous conversation above for context continuity.".to_string(),
));
match provider.chat(&summary_prompt, None, Some(1024), Some(0.3)).await {
match provider
.chat(&summary_prompt, None, Some(1024), Some(0.3))
.await
{
Ok((summary_msg, _)) => {
let summary_text = summary_msg
.content
@@ -373,5 +370,3 @@ pub async fn compact_messages_with_ai<P: ProviderService>(
}
}
}
+6 -19
View File
@@ -45,11 +45,7 @@ use sha2::{Digest, Sha256};
/// and clear them after a successful (or failed) flow completion.
pub trait OAuthFlowStore: Send + Sync {
/// Persist the PKCE code verifier and CSRF state token.
fn save_flow_state(
&self,
verifier: &str,
state: &str,
) -> Result<(), ServiceError>;
fn save_flow_state(&self, verifier: &str, state: &str) -> Result<(), ServiceError>;
/// Load the stored PKCE code verifier.
fn load_verifier(&self) -> Result<String, ServiceError>;
@@ -141,12 +137,7 @@ pub struct OAuthUseCase<R, S, E> {
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S, E> {
/// Create a new OAuth use-case.
pub fn new(
token_repo: R,
flow_store: S,
token_exchanger: E,
token_path: PathBuf,
) -> Self {
pub fn new(token_repo: R, flow_store: S, token_exchanger: E, token_path: PathBuf) -> Self {
OAuthUseCase {
token_repo,
flow_store,
@@ -156,8 +147,8 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S
}
}
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
zesdex_domain::auth::OAuthService for OAuthUseCase<R, S, E>
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> zesdex_domain::auth::OAuthService
for OAuthUseCase<R, S, E>
{
fn start_flow(
&self,
@@ -183,12 +174,8 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
"starting OAuth flow",
);
let mut url = url::Url::parse(&config.auth_url)
.map_err(|e| {
ServiceError::InvalidConfig(format!(
"invalid auth_url '{}': {e}",
config.auth_url
))
let mut url = url::Url::parse(&config.auth_url).map_err(|e| {
ServiceError::InvalidConfig(format!("invalid auth_url '{}': {e}", config.auth_url))
})?;
url.query_pairs_mut()
+6 -11
View File
@@ -46,12 +46,11 @@ impl<R: SessionRepository, L: SessionLockRepository> SessionServiceImpl<R, L> {
}
}
impl<R: SessionRepository, L: SessionLockRepository>
zesdex_domain::auth::SessionService for SessionServiceImpl<R, L>
impl<R: SessionRepository, L: SessionLockRepository> zesdex_domain::auth::SessionService
for SessionServiceImpl<R, L>
{
fn create_session(&self, title: &str) -> Result<Session, ServiceError> {
let id = SessionId::new(&Uuid::new_v4().to_string())
.map_err(ServiceError::Other)?;
let id = SessionId::new(&Uuid::new_v4().to_string()).map_err(ServiceError::Other)?;
let title_owned = if title.is_empty() {
"New Session".to_string()
} else {
@@ -59,8 +58,7 @@ impl<R: SessionRepository, L: SessionLockRepository>
};
let session = Session::new(id.into_string(), title_owned);
tracing::debug!(session_id = %session.id, title = %session.title, "creating new session");
self.session_repo
.save_session(&self.base_dir, &session)?;
self.session_repo.save_session(&self.base_dir, &session)?;
Ok(session)
}
@@ -73,17 +71,14 @@ impl<R: SessionRepository, L: SessionLockRepository>
fn archive_session(&self, id: SessionId) -> Result<(), ServiceError> {
tracing::debug!(session_id = %id, "archiving session");
let mut session = self
.session_repo
.load_session(&self.base_dir, &id)?;
let mut session = self.session_repo.load_session(&self.base_dir, &id)?;
session.archived = true;
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
session.updated_at = i64::try_from(millis).unwrap_or(i64::MAX);
self.session_repo
.save_session(&self.base_dir, &session)?;
self.session_repo.save_session(&self.base_dir, &session)?;
Ok(())
}
}
@@ -58,11 +58,7 @@ impl<R: ConversationRepository> zesdex_domain::cms::ConversationService
Ok(())
}
fn add_message(
&self,
conv: &mut Conversation,
msg: ChatMessage,
) -> Result<(), ServiceError> {
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError> {
tracing::debug!("adding message to session {}", conv.session_id);
conv.push(msg);
let dir = self.session_dir(&conv.session_id);
+5 -14
View File
@@ -14,8 +14,7 @@ use std::path::PathBuf;
use tracing;
use zesdex_domain::cms::{
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings,
SettingsRepository,
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, SettingsRepository,
};
/// Service implementation for settings and app-config operations.
@@ -30,11 +29,7 @@ pub struct SettingsServiceImpl<S, C> {
impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
/// Create a new service with the given repositories and base directory.
pub fn new(
settings_repo: S,
app_config_repo: C,
base_dir: impl Into<PathBuf>,
) -> Self {
pub fn new(settings_repo: S, app_config_repo: C, base_dir: impl Into<PathBuf>) -> Self {
tracing::debug!("creating SettingsServiceImpl");
Self {
settings_repo,
@@ -44,8 +39,8 @@ impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
}
}
impl<S: SettingsRepository, C: AppConfigRepository>
zesdex_domain::cms::SettingsService for SettingsServiceImpl<S, C>
impl<S: SettingsRepository, C: AppConfigRepository> zesdex_domain::cms::SettingsService
for SettingsServiceImpl<S, C>
{
fn load_settings(&self) -> Result<Settings, ServiceError> {
tracing::debug!("loading settings");
@@ -60,11 +55,7 @@ impl<S: SettingsRepository, C: AppConfigRepository>
Ok(())
}
fn update_provider(
&self,
name: &str,
config: &ProviderConfig,
) -> Result<(), ServiceError> {
fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<(), ServiceError> {
tracing::debug!("updating provider '{name}'");
let mut app_config: AppConfig = self.app_config_repo.load(&self.base_dir)?;
app_config
+3 -4
View File
@@ -30,10 +30,10 @@
//! the use-case logic independent of any specific persistence or infrastructure
//! technology.
pub mod agent;
pub mod auth;
pub mod cms;
pub mod ports;
pub mod agent;
// Re-export port traits for ergonomic access.
pub use ports::*;
@@ -46,12 +46,11 @@ pub use auth::{
// Re-export CMS use-cases.
pub use cms::{
conversation_service::ConversationServiceImpl,
memory_service::MemoryServiceImpl,
conversation_service::ConversationServiceImpl, memory_service::MemoryServiceImpl,
settings_service::SettingsServiceImpl,
};
pub use agent::{
turn_service::{compact_messages_with_ai, AgentTurnServiceImpl},
AgentTurnService, ExploreOutput, ExploreService, ToolExecutor,
turn_service::{AgentTurnServiceImpl, compact_messages_with_ai},
};
+2 -5
View File
@@ -22,11 +22,8 @@ pub trait AuthService: Send + Sync {
/// Authenticate a user by verifying a password against a stored hash.
///
/// Returns `true` if the password matches, `false` otherwise.
fn authenticate(
&self,
password: &str,
hash: &str,
) -> impl Future<Output = Result<bool>> + Send;
fn authenticate(&self, password: &str, hash: &str)
-> impl Future<Output = Result<bool>> + Send;
/// Issue a new access + refresh token pair for the given subject.
///
+1 -1
View File
@@ -7,8 +7,8 @@ use std::path::PathBuf;
use crate::core::{ChatMessage, ToolCallResult, UsageStats};
pub mod defaults;
pub mod prompt;
pub mod progress;
pub mod prompt;
/// Which kind of caller (main agent vs. subagent vs. reviewer) is
/// invoking a tool, used to scope permissions and tag log/output paths.
+1 -1
View File
@@ -11,5 +11,5 @@
//! - `ChatMessage` — a single message with role, content, and tool metadata
//! - `Role` — message role enum (User, Assistant, System, Tool)
pub use crate::core::message::{ChatMessage, Role};
pub use crate::core::conversation::Conversation;
pub use crate::core::message::{ChatMessage, Role};
+1 -1
View File
@@ -32,6 +32,7 @@ pub mod settings;
pub use app_config::AppConfig;
pub use app_config::ModelRole;
pub use app_config::ProviderConfig;
pub use commands::{NewMemory, SettingsPatch};
pub use conversation::Conversation;
pub use edit_log::EditLog;
pub use edit_log::EditLogEntry;
@@ -46,7 +47,6 @@ pub use repository::SettingsRepository;
pub use service::ConversationService;
pub use service::MemoryService;
pub use service::SettingsService;
pub use commands::{NewMemory, SettingsPatch};
pub use settings::InternetMode;
pub use settings::Settings;
pub use settings::SettingsFlags;
+1 -5
View File
@@ -56,11 +56,7 @@ pub trait ConversationRepository {
fn load(&self, session_dir: &Path) -> Result<Conversation, RepositoryError>;
/// Persist a `Conversation` to the given session directory.
fn save(
&self,
session_dir: &Path,
conversation: &Conversation,
) -> Result<(), RepositoryError>;
fn save(&self, session_dir: &Path, conversation: &Conversation) -> Result<(), RepositoryError>;
}
/// Persistence contract for `Memory` (long-term agent memory entries).
+1 -5
View File
@@ -44,11 +44,7 @@ pub trait ConversationService {
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError>;
/// Append a single `ChatMessage` to the conversation and persist.
fn add_message(
&self,
conv: &mut Conversation,
msg: ChatMessage,
) -> Result<(), ServiceError>;
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError>;
}
/// Use-cases for long-term memory management.
+6 -7
View File
@@ -335,16 +335,16 @@ impl SseParser {
{
const MAX_TOOL_CALLS: usize = 64;
for tc in tool_calls {
let raw_index =
tc.get("index").and_then(Value::as_u64).unwrap_or_else(
|| {
let raw_index = tc
.get("index")
.and_then(Value::as_u64)
.unwrap_or_else(|| {
tracing::warn!(
"[stream] tool call delta missing index, \
defaulting to 0"
);
0
},
);
});
// Clamp index to prevent out-of-bounds / memory exhaustion
let index = usize::try_from(raw_index)
.unwrap_or(0)
@@ -395,8 +395,7 @@ impl SseParser {
if let Some(content) = delta.get("text").and_then(|c| c.as_str()) {
d_events.push(StreamEvent::Token(content.to_string()));
}
if let Some(reasoning) =
delta.get("reasoning_content").and_then(|r| r.as_str())
if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str())
{
d_events.push(StreamEvent::Reasoning(reasoning.to_string()));
}
+5 -3
View File
@@ -40,9 +40,11 @@ impl Store {
///
/// Why: paths are computed, not created — call `ensure_dirs` before use.
pub fn new() -> Self {
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok()
.or_else(|| std::env::var("HOME").ok().map(|h| format!("{h}/.local/share")))
{
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok().or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| format!("{h}/.local/share"))
}) {
PathBuf::from(data_dir).join("zesdex")
} else {
PathBuf::from(".local/share/zesdex")
+14 -15
View File
@@ -24,33 +24,32 @@
//! no framework imports, no side effects. All persistence is expressed
//! through repository traits that infrastructure adapters implement.
pub mod agent;
pub mod auth;
pub mod cms;
pub mod core;
pub mod error;
pub mod agent;
pub mod workflow;
pub mod subagent;
pub mod workflow;
// Re-export all public items from each module for ergonomic imports.
// Consumers can do `use zesdex_domain::*` for common types.
pub use auth::{
IamSession, NewSession, OAuthConfig, OAuthToken, OAuthRepository, OAuthService,
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session,
SessionId, SessionLock, SessionLockRepository, SessionRepository, SessionService,
IamSession, NewSession, OAuthConfig, OAuthRepository, OAuthService, OAuthToken,
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session, SessionId,
SessionLock, SessionLockRepository, SessionRepository, SessionService,
};
pub use cms::{
AppConfig, AppConfigRepository, Conversation as CmsConversation,
ConversationRepository, ConversationService, EditLog, EditLogEntry,
EditLogRepository, InternetMode, Memory, MemoryRepository, MemoryService,
ModelRole, NewMemory, ProviderConfig, RepositoryError as CmsRepositoryError,
ServiceError as CmsServiceError, Settings, SettingsFlags, SettingsPatch,
SettingsRepository, SettingsService,
AppConfig, AppConfigRepository, Conversation as CmsConversation, ConversationRepository,
ConversationService, EditLog, EditLogEntry, EditLogRepository, InternetMode, Memory,
MemoryRepository, MemoryService, ModelRole, NewMemory, ProviderConfig,
RepositoryError as CmsRepositoryError, ServiceError as CmsServiceError, Settings,
SettingsFlags, SettingsPatch, SettingsRepository, SettingsService,
};
pub use core::{
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role,
SseParser, StreamEvent, StreamOptions, Store, TokenUsage, ToolCall,
ToolCallResult, ToolDef, ToolFunction, ToolFunctionDef, UsageStats,
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role, SseParser, Store,
StreamEvent, StreamOptions, TokenUsage, ToolCall, ToolCallResult, ToolDef, ToolFunction,
ToolFunctionDef, UsageStats,
};
pub use error::DomainError;
@@ -60,5 +59,5 @@ pub use agent::*;
pub use agent::defaults::*;
pub use agent::progress::AgentProgress;
pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive};
pub use workflow::*;
pub use subagent::*;
pub use workflow::*;
+3 -6
View File
@@ -4,7 +4,6 @@
//! configuration files plus a seed session for development/testing.
//! Invoked as `cargo run --bin seed`.
fn main() -> anyhow::Result<()> {
let store = zesdex_domain::core::Store::new();
store.ensure_dirs()?;
@@ -46,13 +45,11 @@ fn main() -> anyhow::Result<()> {
// Create a seed session
let session_id = uuid::Uuid::new_v4().to_string();
let session = zesdex_domain::auth::Session::new(
session_id.clone(),
"Seed Session".to_string(),
);
let session = zesdex_domain::auth::Session::new(session_id.clone(), "Seed Session".to_string());
// Persist via the session repository
use zesdex_domain::SessionRepository;
let repo = zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
let repo =
zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
repo.save_session(&store.base_dir, &session)?;
tracing::info!("Seed session created: id={session_id}");
+4 -1
View File
@@ -16,7 +16,10 @@ struct ClaudeSettings {
}
fn main() {
let path = dirs::home_dir().unwrap().join(".claude").join("settings.json");
let path = dirs::home_dir()
.unwrap()
.join(".claude")
.join("settings.json");
println!("Path: {:?}", path);
match std::fs::read_to_string(&path) {
Ok(content) => {
+5 -1
View File
@@ -12,7 +12,11 @@ use clap::Parser;
/// Zesdex — autonomous AI coding agent.
#[derive(Parser, Debug)]
#[command(name = "zesdex", version, about = "Autonomous AI coding agent with TUI")]
#[command(
name = "zesdex",
version,
about = "Autonomous AI coding agent with TUI"
)]
struct Cli {
/// Run as background daemon with IPC socket
#[arg(long)]
+6 -10
View File
@@ -21,20 +21,13 @@ impl LoopbackServer {
format!("http://127.0.0.1:{}/callback", self.port)
}
pub fn wait_for_code(
&self,
timeout_ms: u64,
expected_state: &str,
) -> std::io::Result<String> {
pub fn wait_for_code(&self, timeout_ms: u64, expected_state: &str) -> std::io::Result<String> {
let (mut stream, _) = self.listener.accept()?;
stream.set_read_timeout(Some(std::time::Duration::from_millis(timeout_ms)))?;
Self::read_callback(&mut stream, expected_state)
}
fn read_callback(
stream: &mut TcpStream,
expected_state: &str,
) -> std::io::Result<String> {
fn read_callback(stream: &mut TcpStream, expected_state: &str) -> std::io::Result<String> {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf)?;
let request = String::from_utf8_lossy(&buf[..n]);
@@ -68,7 +61,10 @@ impl LoopbackServer {
));
}
code.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "code not found in callback")
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"code not found in callback",
)
})
}
@@ -59,17 +59,25 @@ pub struct AuditReport {
impl AuditReport {
/// True if any ERROR-level violations exist.
pub fn has_errors(&self) -> bool {
self.violations.iter().any(|v| v.severity == Severity::Error)
self.violations
.iter()
.any(|v| v.severity == Severity::Error)
}
/// Number of errors.
pub fn error_count(&self) -> usize {
self.violations.iter().filter(|v| v.severity == Severity::Error).count()
self.violations
.iter()
.filter(|v| v.severity == Severity::Error)
.count()
}
/// Number of warnings.
pub fn warning_count(&self) -> usize {
self.violations.iter().filter(|v| v.severity == Severity::Warning).count()
self.violations
.iter()
.filter(|v| v.severity == Severity::Warning)
.count()
}
}
@@ -152,11 +160,7 @@ fn forbidden_imports(layer: &str) -> &'static [&'static str] {
}
/// Scan a single Rust source file for forbidden imports.
fn scan_file(
file_path: &Path,
layer: &'static str,
root: &Path,
) -> Vec<Violation> {
fn scan_file(file_path: &Path, layer: &'static str, root: &Path) -> Vec<Violation> {
let mut violations = Vec::new();
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
@@ -185,10 +189,12 @@ fn scan_file(
// `use crate::` in domain could reference domain-only items — skip.
continue;
}
if trimmed.starts_with(&pattern) || trimmed.starts_with(&format!("use {forbidden}::")) {
if trimmed.starts_with(&pattern)
|| trimmed.starts_with(&format!("use {forbidden}::"))
{
// Skip test code — test modules commonly import outer layers.
let is_test = content[..content.len().saturating_sub(1)]
.contains("#[cfg(test)]");
let is_test =
content[..content.len().saturating_sub(1)].contains("#[cfg(test)]");
if is_test {
continue;
}
@@ -243,10 +249,7 @@ pub fn audit_layering(root: &Path) -> Result<AuditReport> {
}
// Determine which crate this file belongs to by walking up.
let layer = path
.ancestors()
.skip(1)
.find_map(|p| classify_layer(p));
let layer = path.ancestors().skip(1).find_map(|p| classify_layer(p));
if let Some(layer) = layer {
files_scanned += 1;
@@ -371,7 +374,10 @@ mod tests {
fn function_metrics_short_function_ok() {
let content = "fn ok() {\n let x = 1;\n}\n";
let violations = check_function_metrics(content);
let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect();
let long: Vec<_> = violations
.iter()
.filter(|v| v.message.contains("Function too long"))
.collect();
assert!(long.is_empty(), "short function should not trigger");
}
@@ -383,7 +389,10 @@ mod tests {
}
lines.push_str("}\n");
let violations = check_function_metrics(&lines);
let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect();
let long: Vec<_> = violations
.iter()
.filter(|v| v.message.contains("Function too long"))
.collect();
assert!(!long.is_empty(), "long function should trigger warning");
}
}
@@ -36,7 +36,9 @@ pub struct CodeQualityReport {
impl CodeQualityReport {
pub fn has_errors(&self) -> bool {
self.findings.iter().any(|f| f.severity == super::arch_audit::Severity::Error)
self.findings
.iter()
.any(|f| f.severity == super::arch_audit::Severity::Error)
}
pub fn count_by_rule(&self) -> Vec<(&'static str, usize)> {
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
@@ -147,7 +149,8 @@ pub fn scan_quality_file(file_path: &Path, root: &Path) -> Vec<Finding> {
// ── Rule: Missing doc comments on pub items ────────────────────
if (trimmed.starts_with("pub ") || trimmed.starts_with("pub("))
&& !prev_line_doc && !prev_line_empty
&& !prev_line_doc
&& !prev_line_empty
{
// Check it's a struct/enum/fn/trait/type/const/mod
let is_item = trimmed.starts_with("pub fn ")
@@ -246,7 +249,10 @@ mod tests {
std::fs::write(&file, "fn x() { let y = foo.unwrap(); }\n").unwrap();
let findings = scan_quality_file(&file, &dir);
let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect();
let unwrap_findings: Vec<_> = findings
.iter()
.filter(|f| f.rule == "unwrap-in-production")
.collect();
assert!(!unwrap_findings.is_empty(), "should detect unwrap");
}
@@ -262,8 +268,14 @@ mod tests {
.unwrap();
let findings = scan_quality_file(&file, &dir);
let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect();
assert!(unwrap_findings.is_empty(), "should skip unwrap in test blocks");
let unwrap_findings: Vec<_> = findings
.iter()
.filter(|f| f.rule == "unwrap-in-production")
.collect();
assert!(
unwrap_findings.is_empty(),
"should skip unwrap in test blocks"
);
}
#[test]
@@ -274,7 +286,13 @@ mod tests {
std::fs::write(&file, "#[allow(clippy::too_many_arguments)]\nfn x() {}\n").unwrap();
let findings = scan_quality_file(&file, &dir);
let bypass_findings: Vec<_> = findings.iter().filter(|f| f.rule == "compiler-bypass").collect();
assert!(!bypass_findings.is_empty(), "should detect allow attributes");
let bypass_findings: Vec<_> = findings
.iter()
.filter(|f| f.rule == "compiler-bypass")
.collect();
assert!(
!bypass_findings.is_empty(),
"should detect allow attributes"
);
}
}
+23 -16
View File
@@ -50,9 +50,8 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
}
// Parse: `type(scope): description` or `type!: description` or `type: description`
let re = Regex::new(
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$",
)
let re =
Regex::new(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$")
.expect("valid regex for commit parsing");
match re.captures(subject) {
@@ -94,21 +93,19 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
} else {
let first_char = desc.chars().next().unwrap_or(' ');
if first_char.is_uppercase() {
errors.push(format!(
"Description must start with lowercase: '{desc}'"
));
errors.push(format!("Description must start with lowercase: '{desc}'"));
}
if desc.ends_with('.') {
errors.push(format!(
"Description must not end with a period: '{desc}'"
));
errors.push(format!("Description must not end with a period: '{desc}'"));
}
}
// Type-specific rules.
match type_ {
"chore" | "docs" | "refactor" | "test" | "style" | "perf" | "ci" | "build"
| "revert" if scope.is_some() => {
| "revert"
if scope.is_some() =>
{
errors.push(format!(
"'{type_}' commits should not use a scope. \
Only 'feat' and 'fix' require scopes."
@@ -140,9 +137,8 @@ pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
let msg = msg.trim();
let subject = msg.lines().next()?;
let re = Regex::new(
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$",
)
let re =
Regex::new(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$")
.expect("valid regex");
let caps = re.captures(subject)?;
@@ -155,10 +151,18 @@ pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
};
Some(CommitInfo {
type_: caps.name("type").map(|m| m.as_str()).unwrap_or("").to_string(),
type_: caps
.name("type")
.map(|m| m.as_str())
.unwrap_or("")
.to_string(),
scope: caps.name("scope").map(|m| m.as_str().to_string()),
breaking: caps.name("breaking").is_some(),
description: caps.name("desc").map(|m| m.as_str()).unwrap_or("").to_string(),
description: caps
.name("desc")
.map(|m| m.as_str())
.unwrap_or("")
.to_string(),
body,
})
}
@@ -232,7 +236,10 @@ mod tests {
#[test]
fn parses_valid_commit() {
let parsed = parse_commit_message("feat(agent): add parallel execution\n\nWith cycle orchestration.").unwrap();
let parsed = parse_commit_message(
"feat(agent): add parallel execution\n\nWith cycle orchestration.",
)
.unwrap();
assert_eq!(parsed.type_, "feat");
assert_eq!(parsed.scope, Some("agent".to_string()));
assert!(!parsed.breaking);
@@ -51,7 +51,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [
4. Identify main entry points (main.rs, main.py, index.ts, etc.).\n\
5. Count files by extension type.\n\
Use the ls_dir, read, grep, and glob tools. Be concise.",
// Agent 1: Symbol Index
"You are a symbol index explorer.\n\
1. Call the 'rebuild_index' tool to rebuild the symbol index.\n\
@@ -59,7 +58,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [
3. Identify public APIs, entry points, and key types.\n\
4. Group symbols by language and kind.\n\
Be concise. Report what symbols exist and where they live.",
// Agent 2: Semantic Context
"You are a semantic context explorer.\n\
1. Call the 'rebuild_index' tool to ensure the index is fresh.\n\
@@ -231,8 +229,8 @@ async fn run_explore_phase(
let tc = tool_ctx.clone();
let handle = thread::spawn(move || {
let rt = tokio::runtime::Runtime::new()
.context("create explore subagent tokio runtime")?;
let rt =
tokio::runtime::Runtime::new().context("create explore subagent tokio runtime")?;
rt.block_on(run_agent(ctx, &directive, AccessTier::Read, tc))
});
@@ -252,12 +250,22 @@ async fn run_explore_phase(
}
Ok(Err(e)) => {
warn!(agent = i, error = %e, "explore subagent failed");
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], &e.to_string());
emit_failed(
&turn_events_clone,
EXPLORE_IDS[i],
EXPLORE_LABELS[i],
&e.to_string(),
);
(i, format!("Error: {e}"), false)
}
Err(e) => {
warn!(agent = i, error = ?e, "explore subagent panicked");
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], "thread panicked");
emit_failed(
&turn_events_clone,
EXPLORE_IDS[i],
EXPLORE_LABELS[i],
"thread panicked",
);
(i, format!("Thread panic: {e:?}"), false)
}
};
@@ -281,9 +289,7 @@ fn build_explore_context(results: &[(usize, String, bool)]) -> String {
let success_count = results.iter().filter(|r| r.2).count();
let total = results.len();
let mut msg = format!(
"[Explore Phase — {success_count}/{total} agents succeeded]\n\n"
);
let mut msg = format!("[Explore Phase — {success_count}/{total} agents succeeded]\n\n");
for (i, output, success) in results {
let label = EXPLORE_LABELS.get(*i).unwrap_or(&"❓ Unknown");
+1 -4
View File
@@ -107,10 +107,7 @@ impl BestPracticeEngine {
let layering = self.audit_layering(workspace_root)?;
let quality = self.scan_quality(workspace_root)?;
Ok(CombinedAuditReport {
layering,
quality,
})
Ok(CombinedAuditReport { layering, quality })
}
}
+3 -1
View File
@@ -89,6 +89,8 @@ impl BashJob {
let Ok(mut guard) = self.process.lock() else {
return false;
};
guard.as_mut().is_some_and(|c| matches!(c.try_wait(), Ok(None)))
guard
.as_mut()
.is_some_and(|c| matches!(c.try_wait(), Ok(None)))
}
}
+8 -10
View File
@@ -7,24 +7,22 @@
pub fn check_dangerous_pattern(tool_name: &str, args: &serde_json::Value) -> Option<String> {
match tool_name {
"bash" => {
let cmd = args
.get("command")
.and_then(|v| v.as_str())
.unwrap_or("");
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
// Detect git push with --force
if cmd.contains("git push") && cmd.contains("--force") {
return Some("Force-pushing to git is destructive and may lose history".to_string());
return Some(
"Force-pushing to git is destructive and may lose history".to_string(),
);
}
// Detect rm -rf /
if cmd.contains("rm -rf /") || cmd.contains("rm -rf /*") {
return Some("Recursive deletion of the root filesystem is never allowed".to_string());
return Some(
"Recursive deletion of the root filesystem is never allowed".to_string(),
);
}
}
"delete" => {
let path = args
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("");
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
if path == "/" || path.starts_with("/etc") {
return Some(format!("Deleting '{}' is too dangerous", path));
}
+1 -4
View File
@@ -76,10 +76,7 @@ pub struct StatePayload {
pub enum DaemonFrame {
StateUpdate(Box<StatePayload>),
StreamToken(String),
SystemNote {
kind: String,
message: String,
},
SystemNote { kind: String, message: String },
ClipboardCopy(String),
Closed,
}
+1 -2
View File
@@ -67,8 +67,7 @@ use std::sync::Arc;
pub use tools::{GraduatedCheck, Tool, ToolCtx, ToolCtxBuilder};
// Re-export commonly needed types at the crate root
pub use zesdex_domain::core::{ChatMessage, Role, Store, UsageStats, ToolCallResult};
pub use zesdex_domain::core::{ChatMessage, Role, Store, ToolCallResult, UsageStats};
/// A shared, async-writable cache of directory entries, used to avoid
/// re-reading a directory every render frame.
+14 -21
View File
@@ -4,10 +4,10 @@
use rand_core::RngCore;
use std::time::Duration;
use zesdex_application::ports::ProviderService;
use zesdex_domain::core::{
ChatMessage, ChatRequest, ChatResponse, SseParser, StreamEvent, StreamOptions, ToolDef,
};
use zesdex_application::ports::ProviderService;
use zesdex_domain::agent::defaults::{DEFAULT_API_BASE, DEFAULT_MODEL};
const DEFAULT_BASE_URL: &str = DEFAULT_API_BASE;
@@ -82,10 +82,7 @@ impl LlmClient {
retrying without connect timeout",
e,
);
match reqwest::Client::builder()
.timeout(REQUEST_TIMEOUT)
.build()
{
match reqwest::Client::builder().timeout(REQUEST_TIMEOUT).build() {
Ok(c) => c,
Err(e2) => {
tracing::warn!("also failed: {e2}. using default client");
@@ -115,8 +112,7 @@ impl LlmClient {
.post(url)
.header("Content-Type", "application/json");
if !self.api_key.is_empty() {
http_req =
http_req.header("Authorization", format!("Bearer {}", self.api_key));
http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
}
let mut resp = http_req.json(req).send().await.map_err(|e| {
@@ -237,8 +233,7 @@ impl LlmClient {
n
}
};
let text =
String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned();
let text = String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned();
byte_buf.drain(..valid_len);
for event in parser.feed(&text) {
@@ -306,8 +301,7 @@ impl ProviderService for LlmClient {
.header("Content-Type", "application/json");
if !self.api_key.is_empty() {
http_req =
http_req.header("Authorization", format!("Bearer {}", self.api_key));
http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
}
let result = async {
@@ -335,9 +329,9 @@ impl ProviderService for LlmClient {
}
let data: ChatResponse = resp.json().await?;
let usage = data.usage.map(|u| {
(u64::from(u.prompt_tokens), u64::from(u.completion_tokens))
});
let usage = data
.usage
.map(|u| (u64::from(u.prompt_tokens), u64::from(u.completion_tokens)));
let message = data
.choices
.into_iter()
@@ -345,7 +339,8 @@ impl ProviderService for LlmClient {
.and_then(|c| c.message)
.ok_or_else(|| anyhow::anyhow!("API response had no choices"))?;
Ok((message, usage))
}.await;
}
.await;
match result {
Ok((msg, usage)) => return Ok((msg, usage)),
@@ -393,7 +388,9 @@ impl ProviderService for LlmClient {
let mut captured_content = false;
let mut wrapped = |event: &StreamEvent| -> bool {
match event {
StreamEvent::Token(_) | StreamEvent::Reasoning(_) | StreamEvent::ToolCallDelta { .. } => {
StreamEvent::Token(_)
| StreamEvent::Reasoning(_)
| StreamEvent::ToolCallDelta { .. } => {
captured_content = true;
}
_ => {}
@@ -425,11 +422,7 @@ pub fn resolve_api_key(
) -> String {
let provider = &settings.provider;
let mut api_key = settings
.api_keys
.get(provider)
.cloned()
.unwrap_or_default();
let mut api_key = settings.api_keys.get(provider).cloned().unwrap_or_default();
if api_key.is_empty() {
if let Some(provider_cfg) = app_config.providers.get(provider) {
+10 -2
View File
@@ -32,8 +32,16 @@ impl LspClient {
.stderr(Stdio::piped())
.spawn()?;
let stdin = child.stdin.take().ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?;
let stdout = BufReader::new(child.stdout.take().ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?);
let stdin = child
.stdin
.take()
.ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?;
let stdout = BufReader::new(
child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?,
);
info!("LSP client spawned: {command}");
Ok(LspClient {
-1
View File
@@ -28,7 +28,6 @@ impl Default for LspManager {
}
impl LspManager {
pub fn start(&mut self, language: &str, command: &str, args: &[String]) -> anyhow::Result<()> {
let client = LspClient::start(command, args)?;
self.clients.insert(language.to_string(), client);
@@ -9,8 +9,14 @@ fn known_configs() -> HashMap<&'static str, (&'static str, Vec<&'static str>)> {
let mut m = HashMap::new();
m.insert("rust", ("rust-analyzer", vec![]));
m.insert("python", ("pyright-langserver", vec!["--stdio"]));
m.insert("typescript", ("typescript-language-server", vec!["--stdio"]));
m.insert("javascript", ("typescript-language-server", vec!["--stdio"]));
m.insert(
"typescript",
("typescript-language-server", vec!["--stdio"]),
);
m.insert(
"javascript",
("typescript-language-server", vec!["--stdio"]),
);
m.insert("go", ("gopls", vec![]));
m
}
@@ -14,7 +14,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result<String> {
if output.status.success() {
Ok("rust-analyzer installed via rustup".to_string())
} else {
anyhow::bail!("failed to install rust-analyzer: {}", String::from_utf8_lossy(&output.stderr))
anyhow::bail!(
"failed to install rust-analyzer: {}",
String::from_utf8_lossy(&output.stderr)
)
}
}
"python" => {
@@ -24,7 +27,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result<String> {
if output.status.success() {
Ok("pyright installed via npm".to_string())
} else {
anyhow::bail!("failed to install pyright: {}", String::from_utf8_lossy(&output.stderr))
anyhow::bail!(
"failed to install pyright: {}",
String::from_utf8_lossy(&output.stderr)
)
}
}
lang => anyhow::bail!("no install method known for language '{lang}'"),
@@ -1,21 +1,21 @@
//! High-level manager that discovers, installs (if needed), and starts
//! LSP servers.
use crate::lsp::manager::LspManager;
use super::discovery::discover_installed;
use super::install::install_language_server;
use crate::lsp::manager::LspManager;
/// Auto-provision language servers for the given list of languages.
///
/// Flow: discover already-installed servers → for each requested language
/// not yet available, attempt auto-install → start each server.
pub fn auto_provision(
lsp_manager: &mut LspManager,
languages: &[String],
) -> Vec<String> {
pub fn auto_provision(lsp_manager: &mut LspManager, languages: &[String]) -> Vec<String> {
let mut started = Vec::new();
let installed = discover_installed();
let mut installed_map: std::collections::HashMap<&str, &crate::lsp::provisioner::config::LspProvisionerConfig> = std::collections::HashMap::new();
let mut installed_map: std::collections::HashMap<
&str,
&crate::lsp::provisioner::config::LspProvisionerConfig,
> = std::collections::HashMap::new();
for cfg in &installed {
installed_map.insert(cfg.language.as_str(), cfg);
}
-1
View File
@@ -31,7 +31,6 @@ impl Default for McpManager {
}
impl McpManager {
/// Register an MCP server by name and transport string.
///
/// Returns an error if a server with the same name is already registered.
+5 -1
View File
@@ -114,7 +114,11 @@ where
}
Box::pin(async move {
Ok((StatusCode::UNAUTHORIZED, "missing or invalid X-Session-Id header").into_response())
Ok((
StatusCode::UNAUTHORIZED,
"missing or invalid X-Session-Id header",
)
.into_response())
})
}
}
@@ -1,26 +0,0 @@
//! CORS layer factory for the daemon HTTP server.
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
/// Return a permissive CorsLayer for local daemon IPC.
///
/// All method and header names are static strings guaranteed to be valid
/// HTTP tokens — `.parse()` is infallible here.
pub fn default_cors_layer() -> CorsLayer {
CorsLayer::new()
.allow_origin(AllowOrigin::any())
.allow_methods([
"GET".parse().expect("static HTTP method"),
"POST".parse().expect("static HTTP method"),
"PUT".parse().expect("static HTTP method"),
"DELETE".parse().expect("static HTTP method"),
"PATCH".parse().expect("static HTTP method"),
"OPTIONS".parse().expect("static HTTP method"),
])
.allow_headers(AllowHeaders::any())
.expose_headers([
"Content-Type".parse().expect("static HTTP header"),
"X-Session-Id".parse().expect("static HTTP header"),
"X-Request-Id".parse().expect("static HTTP header"),
])
}
@@ -1,5 +1,4 @@
//! Axum middleware tower for the HTTP API layer.
pub mod auth;
pub mod cors;
pub mod rate_limit;
@@ -28,11 +28,14 @@ impl RateLimiter {
.as_secs() as i64;
let cutoff = now.saturating_sub(window_secs as i64);
let mut windows = self.windows.lock().map_err(|e| {
anyhow::anyhow!("rate limiter lock poisoned: {e}")
})?;
let mut windows = self
.windows
.lock()
.map_err(|e| anyhow::anyhow!("rate limiter lock poisoned: {e}"))?;
let timestamps = windows.entry(client_id.to_string()).or_insert_with(Vec::new);
let timestamps = windows
.entry(client_id.to_string())
.or_insert_with(Vec::new);
timestamps.retain(|&ts| ts >= cutoff);
if timestamps.len() >= max_requests as usize {
@@ -3,7 +3,9 @@
use std::path::Path;
use serde::{Deserialize, Serialize};
use zesdex_domain::cms::{AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError};
use zesdex_domain::cms::{
AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError,
};
use crate::utils::write_json_atomic;
@@ -43,7 +45,10 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)>
let file_creds = settings.as_ref().and_then(|s| {
let env = s.env.as_ref()?;
Some((env.anthropic_base_url.clone()?, env.anthropic_api_key.clone()?))
Some((
env.anthropic_base_url.clone()?,
env.anthropic_api_key.clone()?,
))
});
let env_creds = || -> Option<(String, String)> {
@@ -63,7 +68,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)>
default_model: custom_model.clone(),
default_api_key: Some(key),
},
custom_model
custom_model,
))
}
@@ -72,9 +77,7 @@ impl AppConfigRepository for JsonAppConfigRepository {
let path = base_dir.join("app_config.json");
let mut cfg: AppConfig = match std::fs::read_to_string(&path) {
Ok(s) => serde_json::from_str(&s)?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
AppConfig::default()
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => AppConfig::default(),
Err(e) => return Err(RepositoryError::Io(e)),
};
@@ -106,9 +109,7 @@ impl AppConfigRepository for JsonAppConfigRepository {
}
if let Some(custom) = &custom_model {
cfg.model_roles
.entry(custom.clone())
.or_insert(ModelRole {
cfg.model_roles.entry(custom.clone()).or_insert(ModelRole {
provider: "claude".to_string(),
model: custom.clone(),
max_tokens: Some(8192),
@@ -77,10 +77,7 @@ impl MarkdownMemoryRepository {
.lines()
.filter_map(|l| {
let mut it = l.splitn(2, ':');
Some((
it.next()?.trim().to_string(),
it.next()?.trim().to_string(),
))
Some((it.next()?.trim().to_string(), it.next()?.trim().to_string()))
})
.collect()
}
@@ -175,7 +172,9 @@ impl MemoryRepository for MarkdownMemoryRepository {
fn save(&self, memory_dir: &Path, memory: &Memory) -> Result<(), RepositoryError> {
let path = Memory::path(memory_dir, &memory.name);
let parent = path.parent().expect("memory path always has a parent directory");
let parent = path
.parent()
.expect("memory path always has a parent directory");
std::fs::create_dir_all(parent)?;
let frontmatter = Self::build_frontmatter(memory);
@@ -21,16 +21,16 @@ impl SettingsRepository for JsonSettingsRepository {
fn load(&self, base_dir: &Path) -> Result<Settings, RepositoryError> {
let path = base_dir.join("settings.json");
match std::fs::read_to_string(&path) {
Ok(s) => match serde_json::from_str(&s) {
Ok(s) => {
match serde_json::from_str(&s) {
Ok(settings) => Ok(settings),
Err(e) => {
tracing::warn!("settings.json at '{:?}' failed to parse ({e}); falling back to defaults", path);
Ok(Settings::default())
}
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Ok(Settings::default())
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Settings::default()),
Err(e) => Err(RepositoryError::Io(e)),
}
}
@@ -50,9 +50,7 @@ impl SessionLockRepository for FileSystemSessionLockRepository {
.write(true)
.open(&tmp)
.map_err(|_| {
RepositoryError::Other(
"another process is replacing the lock".to_string(),
)
RepositoryError::Other("another process is replacing the lock".to_string())
})?;
write!(tmp_file, "{pid}")?;
tmp_file.sync_all()?;
+6 -10
View File
@@ -5,16 +5,12 @@ pub mod cms;
pub mod iam;
pub mod sqlite;
pub use cms::{
app_config_repo::JsonAppConfigRepository, conversation_repo::JsonConversationRepository,
edit_log_repo::JsonlEditLogRepository, memory_repo::MarkdownMemoryRepository,
rewind_blob_repo::FileRewindBlobRepository, settings_repo::JsonSettingsRepository,
};
pub use iam::{
oauth_repo::FileSystemOAuthRepository,
session_lock_repo::FileSystemSessionLockRepository,
oauth_repo::FileSystemOAuthRepository, session_lock_repo::FileSystemSessionLockRepository,
session_repo::FileSystemSessionRepository,
};
pub use cms::{
app_config_repo::JsonAppConfigRepository,
conversation_repo::JsonConversationRepository,
edit_log_repo::JsonlEditLogRepository,
memory_repo::MarkdownMemoryRepository,
rewind_blob_repo::FileRewindBlobRepository,
settings_repo::JsonSettingsRepository,
};
@@ -160,7 +160,8 @@ pub fn spawn_background_review(
SEVERITY: HIGH|MEDIUM|LOW\n\
OLD: <exact text to replace>\n\
NEW: <replacement text>\n\
---".to_string(),
---"
.to_string(),
);
let user_msg = ChatMessage::user(format!(
@@ -210,7 +211,11 @@ pub fn spawn_background_review(
&turn_events,
TurnEvent::SystemNote {
kind: "review_finding".into(),
message: format!("📋 Auto-review complete ({} fix(es) applied).\n{}", fix_count, response_text.trim()),
message: format!(
"📋 Auto-review complete ({} fix(es) applied).\n{}",
fix_count,
response_text.trim()
),
},
);
info!(fix_count, "auto-review: completed with fixes");
@@ -237,11 +242,7 @@ async fn run_llm_review(client: &LlmClient, messages: &[ChatMessage]) -> Result<
}
/// Parse the LLM response for structured fix commands and apply them.
fn apply_fixes_from_response(
response: &str,
tools: &[Box<dyn Tool>],
tool_ctx: &ToolCtx,
) -> usize {
fn apply_fixes_from_response(response: &str, tools: &[Box<dyn Tool>], tool_ctx: &ToolCtx) -> usize {
let mut fix_count = 0;
// Parse structured fix blocks
+5 -7
View File
@@ -98,10 +98,7 @@ pub async fn run_agent(
// If no tool calls, we're done — return content
if tool_calls.is_empty() {
info!("Subagent completed after {iteration} iterations");
report_progress(
&tool_ctx,
AgentProgress::completed("subagent", directive),
);
report_progress(&tool_ctx, AgentProgress::completed("subagent", directive));
return Ok(content);
}
@@ -121,8 +118,7 @@ pub async fn run_agent(
),
);
let result =
if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) {
let result = if let Some(tool) = tools.iter().find(|t| t.name() == tool_name) {
match tool.run(&tool_ctx, &args) {
Ok(output) => output,
Err(e) => format!("Error: {e}"),
@@ -149,5 +145,7 @@ pub async fn run_agent(
format!("iteration limit ({MAX_ITERATIONS})"),
),
);
Ok(format!("Subagent reached iteration limit ({MAX_ITERATIONS})"))
Ok(format!(
"Subagent reached iteration limit ({MAX_ITERATIONS})"
))
}
+1 -3
View File
@@ -40,9 +40,7 @@ impl SubagentProvider {
messages: &[ChatMessage],
) -> Result<(ChatMessage, Option<(u64, u64)>)> {
use zesdex_application::ports::ProviderService;
self.client
.chat(messages, None, Some(4096), None)
.await
self.client.chat(messages, None, Some(4096), None).await
}
/// Send messages with available tool definitions.
+22 -6
View File
@@ -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}"
);
}
}
+2 -4
View File
@@ -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>>,
}

Some files were not shown because too many files have changed in this diff Show More