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