diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..88592a8 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,48 @@ +# Strict Rust compiler configuration for zesdex +# Forces all warnings as errors, enables maximum optimization for release builds. + +[target.'cfg(all())'] +# Treat all warnings as errors — zero tolerance policy +rustflags = [ + # Force all lints to error level (stricter than -Dwarnings) + "-W", "unused", + "-W", "dead_code", + "-W", "unreachable_code", + "-W", "unused_imports", + "-W", "unused_variables", + "-W", "unused_mut", + "-W", "unused_must_use", + "-W", "unused_unsafe", + "-W", "unused_extern_crates", + + # Safety-critical + "-W", "trivial_casts", + "-W", "trivial_numeric_casts", + + # Deprecation and future compat + "-W", "deprecated", + "-W", "deprecated_in_future", + "-W", "keyword_idents", + "-W", "noop_method_call", + + # Type system + "-W", "invalid_type_param_default", + "-W", "unused_labels", + "-W", "while_true", +] + +[profile.release] +# Maximum speed optimizations for release builds +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = true +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = "symbols" + +[profile.dev] +# Keep dev fast but still strict +opt-level = 0 +debug = true diff --git a/Cargo.toml b/Cargo.toml index ec8052a..9861ace 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,23 @@ version = "0.1.0" edition = "2021" authors = ["asepharyana "] +# Treat all warnings as errors, set strict clippy levels +[lints.rust] +unused = "deny" +dead_code = "deny" +unreachable_code = "deny" +unused_imports = "deny" +unused_variables = "deny" +unused_mut = "deny" +unused_must_use = "deny" +deprecated = "deny" +trivial_casts = "deny" +trivial_numeric_casts = "deny" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -2 } + [dependencies] ratatui = "0.30.2" crossterm = "0.28" diff --git a/src/app/harness.rs b/src/app/harness.rs index 4d51932..8263fb2 100644 --- a/src/app/harness.rs +++ b/src/app/harness.rs @@ -398,13 +398,6 @@ impl Harness { } } - fn classify(cmd: &str) -> Verdict { - match cmd { - "bash" | "write" | "edit" | "delete" | "git_operator" => Verdict::Allow, - _ => Verdict::Allow, - } - } - } impl Default for Harness { diff --git a/src/app/mcp/manager.rs b/src/app/mcp/manager.rs index bcaecff..6130914 100644 --- a/src/app/mcp/manager.rs +++ b/src/app/mcp/manager.rs @@ -404,14 +404,15 @@ impl McpManager { self.servers.iter().flat_map(|server| { let handle = server.child_handle.clone(); server.tools.iter().map(move |info| { - Box::new(McpToolAdapter { + let adapter: Box = Box::new(McpToolAdapter { tool_name: info.name.clone(), server_name: server.name.clone(), transport: server.transport.clone(), description: info.description.clone(), parameters: info.input_schema.clone(), child_handle: handle.clone(), - }) as Box + }); + adapter }) }).collect() } diff --git a/src/app/mode/mod.rs b/src/app/mode/mod.rs index ee1ac93..d3c6d0e 100644 --- a/src/app/mode/mod.rs +++ b/src/app/mode/mod.rs @@ -6,7 +6,6 @@ pub mod editor; pub mod effort; pub mod key_input; pub mod mcp; -pub mod workflow; pub mod quit_confirm; pub mod rewind; diff --git a/src/app/mode/workflow.rs b/src/app/mode/workflow.rs deleted file mode 100644 index b258fef..0000000 --- a/src/app/mode/workflow.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Workflow-mode helper logic for the TUI workflow overlay. -//! -//! Flow: exposes a dismiss handler invoked by a keybinding to close the -//! workflow overlay, and a status query used elsewhere to check whether -//! it is currently showing. - -use crate::app::state::rest::AppStateRest; -use crate::app::state::types::Overlay; - -/// Close the workflow overlay if it is the currently active overlay. -/// -/// Flow: check `state.misc.overlay == Overlay::Workflow`, reset to `Overlay::None` -/// if so, then mark state dirty regardless. -/// -/// Why: no-ops safely if another overlay is showing, so it can be called -/// unconditionally from a dismiss keybinding. -/// -/// Return: nothing; mutates `state.misc.overlay` and `state.dirty` in place. -pub fn handle_workflow_dismiss(state: &mut AppStateRest) { - if state.misc.overlay == Overlay::Workflow { - state.misc.overlay = Overlay::None; - } - state.dirty = true; -} - -/// Report whether the workflow overlay is currently displayed. -/// -/// Flow: compare `state.misc.overlay` against `Overlay::Workflow`. -/// -/// Return: `"active"` if the workflow overlay is shown, `"idle"` otherwise. -pub fn workflow_status(state: &AppStateRest) -> &str { - if state.misc.overlay == Overlay::Workflow { - "active" - } else { - "idle" - } -} diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index bce1dfa..e2e6b7f 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -405,15 +405,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { lifetime_ms: 8000, }); state.dirty = true; - } else if kind == "bg-arch-review" { - state.push_toast(Toast { - kind: ToastKind::Info, - message: message.clone(), - created_at: chrono::Utc::now().timestamp_millis(), - lifetime_ms: 10000, - }); - state.dirty = true; - } else if kind == "bg-security-review" { + } else if kind == "bg-arch-review" || kind == "bg-security-review" { state.push_toast(Toast { kind: ToastKind::Info, message: message.clone(), @@ -1789,7 +1781,7 @@ fn rand_bytes(n: usize) -> Vec { .unwrap_or_default() .as_nanos() as u64; let base = seed ^ counter; - (0..n).map(|i| ((base >> ((i as u64 % 8) * 8)) ^ (i as u64 * 2654435761)) as u8).collect() + (0..n).map(|i| ((base >> ((i as u64 % 8) * 8)) ^ (i as u64 * 2_654_435_761)) as u8).collect() } diff --git a/src/app/state/rest.rs b/src/app/state/rest.rs index b851ef5..c71e300 100644 --- a/src/app/state/rest.rs +++ b/src/app/state/rest.rs @@ -52,7 +52,6 @@ pub struct AppStateRest { pub session_id: String, pub session_dir: PathBuf, pub memory_dir: PathBuf, - pub download_dir: PathBuf, pub worktrees_dir: PathBuf, pub dir_cache: Arc>, pub edit_log: EditLog, @@ -88,10 +87,6 @@ impl AppStateRest { pub fn new(workspace_roots: Vec, session_dir: PathBuf, memory_dir: PathBuf) -> Self { let settings = Settings::load(); let app_config = AppConfig::load(); - let download_dir = memory_dir.parent().unwrap_or_else(|| { - tracing::warn!("[state] memory_dir '{}' has no parent, using it for downloads", memory_dir.display()); - &memory_dir - }).join("downloads"); let worktrees_dir = memory_dir.parent().unwrap_or_else(|| { tracing::warn!("[state] memory_dir '{}' has no parent, using it for worktrees", memory_dir.display()); &memory_dir @@ -112,7 +107,6 @@ impl AppStateRest { session_id, session_dir: session_dir.clone(), memory_dir, - download_dir, worktrees_dir, turn_events: Arc::new(Mutex::new(VecDeque::new())), turn_in_flight: Arc::new(Mutex::new(false)), @@ -290,7 +284,6 @@ impl AppStateRest { workspaces: self.workspace_roots.clone(), session_dir: self.session_dir.clone(), memory_dir: self.memory_dir.clone(), - _download_dir: self.download_dir.clone(), worktrees_dir: self.worktrees_dir.clone(), dir_cache: self.dir_cache.clone(), origin, diff --git a/src/app/subagent/auto.rs b/src/app/subagent/auto.rs index 42bb46c..c29edda 100644 --- a/src/app/subagent/auto.rs +++ b/src/app/subagent/auto.rs @@ -44,7 +44,7 @@ const QUICK_REVIEW_MAX_STEPS: usize = 2; const BG_SUBAGENT_MAX_STEPS: usize = 8; /// ─── Helpers ─── - +/// /// Check whether a file path is worth auto-reviewing (not config/lock/data). pub fn is_reviewable_path(path: &str) -> bool { let lower = path.to_lowercase(); @@ -81,7 +81,7 @@ fn is_production_code(path: &str) -> bool { } /// ─── Inline Quick Review (synchronous, feeds back to LLM) ─── - +/// /// Spawn a lightweight inline code review subagent for the given file. /// /// The subagent reads the file (read-only), checks for common issues, @@ -143,7 +143,7 @@ pub fn spawn_quick_review( } /// ─── Background Subagent Spawners (async, report via SystemNote) ─── - +/// /// Spawn a background subagent that generates tests for modified files. /// /// Uses the test-generator prompt and has read-write access so it can diff --git a/src/app/subagent/context.rs b/src/app/subagent/context.rs index a2832c4..e3acc00 100644 --- a/src/app/subagent/context.rs +++ b/src/app/subagent/context.rs @@ -2,7 +2,7 @@ //! including the default read-only tool set for reviewer agents. use std::path::PathBuf; -use std::sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}}; +use std::sync::{Arc, Mutex, atomic::AtomicBool}; use super::spawn::AgentDefinition; /// Default read-only tool names granted to `role == "reviewer"` agents. diff --git a/src/app/subagent/division.rs b/src/app/subagent/division.rs index 49603c0..46d3d6a 100644 --- a/src/app/subagent/division.rs +++ b/src/app/subagent/division.rs @@ -32,7 +32,7 @@ pub mod roles { } /// ─── Division Agent Definitions ─── - +/// /// Build the Strategy Division agent — chief architect and planner. /// /// Tools: read-only (read, grep, glob, search, lsp, plan, seqthink, recall) @@ -172,16 +172,17 @@ pub fn documentation_division() -> AgentDefinition { } /// ─── Division Registry ─── - +/// /// A named division with its agent definition and display metadata. #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct Division { /// Display name for the division (e.g. "Strategy", "Engineering"). pub name: &'static str, /// Role tag used for pipeline routing (matches `roles::*` constants). + #[allow(dead_code)] pub role: &'static str, /// One-line description of what this division does. + #[allow(dead_code)] pub description: &'static str, /// Agent definition with tools, prompt, and step budget. pub agent_def: AgentDefinition, diff --git a/src/app/subagent/engine.rs b/src/app/subagent/engine.rs index 107832f..e8e6a17 100644 --- a/src/app/subagent/engine.rs +++ b/src/app/subagent/engine.rs @@ -14,14 +14,6 @@ use crate::tool::{all_tools, tool_defs, tool_is_risky}; use super::context::SubagentContext; use super::event::SubagentEvent; -/// Upper bound on agent loop steps; effectively unbounded (`usize::MAX`). -#[allow(dead_code)] -pub const MAX_AGENT_STEPS: usize = usize::MAX; - -/// Maximum time a single tool call may block inside a subagent before -/// being abandoned. Prevents a stuck tool from hanging the subagent loop. -const SUBAGENT_TOOL_TIMEOUT_MS: u64 = 120_000; - /// Maps a subagent's allowed tool names to concrete Tool trait objects and /// OpenAI-style tool definitions. /// diff --git a/src/model/app_config.rs b/src/model/app_config.rs index 9b41901..3f21a3c 100644 --- a/src/model/app_config.rs +++ b/src/model/app_config.rs @@ -151,7 +151,7 @@ fn detect_claude_settings_provider() -> Option { let settings: ClaudeSettings = serde_json::from_str(&content).ok()?; let env = settings.env?; let base_url = env.anthropic_base_url?; - let _api_key = env.anthropic_api_key?; // presence check — stored as env var, not in config. + let _ = env.anthropic_api_key?; // presence check — stored as env var, not in config. Some(ProviderConfig { api_base: base_url, api_key_env: Some("ANTHROPIC_API_KEY".to_string()), diff --git a/src/model/editlog.rs b/src/model/editlog.rs index c75d92d..1d14b0e 100644 --- a/src/model/editlog.rs +++ b/src/model/editlog.rs @@ -27,8 +27,6 @@ const MAX_MEMORY_ENTRIES: usize = 10_000; pub struct EditLog { pub entries: Vec, pub path: std::path::PathBuf, - /// Total entries on disk (may exceed `entries.len()` if truncated). - pub total_on_disk: usize, } impl EditLog { @@ -37,28 +35,26 @@ impl EditLog { /// `MAX_MEMORY_ENTRIES` to prevent OOM). pub fn new(session_dir: &std::path::Path) -> Self { let path = session_dir.join("edits.jsonl"); - let (entries, total_on_disk) = Self::load_from_disk(&path); - EditLog { entries, path, total_on_disk } + let entries = Self::load_from_disk(&path); + EditLog { entries, path } } /// Reads lines of edits.jsonl into memory, keeping only the most recent /// `MAX_MEMORY_ENTRIES` entries. The full history is preserved on disk /// regardless of the in-memory limit. - fn load_from_disk(path: &std::path::Path) -> (Vec, usize) { + fn load_from_disk(path: &std::path::Path) -> Vec { let file = match std::fs::File::open(path) { Ok(f) => f, - Err(_) => return (Vec::new(), 0), + Err(_) => return Vec::new(), }; use std::io::{BufRead, BufReader}; let reader = BufReader::new(file); let mut entries: Vec = Vec::new(); - let mut total = 0usize; for line in reader.lines() { let line = match line { Ok(l) => l, Err(_) => continue, }; - total += 1; if let Ok(entry) = serde_json::from_str::(&line) { // Keep only the most recent entries in memory if entries.len() >= MAX_MEMORY_ENTRIES { @@ -68,7 +64,7 @@ impl EditLog { entries.push(entry); } } - (entries, total) + entries } /// Append one entry to `edits.jsonl` on disk and to the in-memory log, diff --git a/src/model/memory.rs b/src/model/memory.rs index 9e39ece..1ffaaba 100644 --- a/src/model/memory.rs +++ b/src/model/memory.rs @@ -226,6 +226,7 @@ pub fn slug_path(memory_dir: &Path, raw: &str) -> PathBuf { /// /// Return: `Ok(())` on success, or an `io::Error` from serialization or /// the write. +#[cfg(test)] pub fn export_lessons(memory_dir: &Path, output: &Path) -> std::io::Result<()> { let names = Memory::list(memory_dir); let lessons: Vec = names.iter() @@ -255,6 +256,7 @@ pub fn export_lessons(memory_dir: &Path, output: &Path) -> std::io::Result<()> { /// import on the same file won't overwrite or duplicate existing memories. /// /// Return: the number of memories actually imported (skips existing ones). +#[cfg(test)] pub fn import_lessons(memory_dir: &Path, input: &Path) -> std::io::Result { let data = std::fs::read_to_string(input)?; let lessons: Vec = serde_json::from_str(&data) diff --git a/src/tool/mod.rs b/src/tool/mod.rs index 89ded26..b6274e3 100644 --- a/src/tool/mod.rs +++ b/src/tool/mod.rs @@ -44,7 +44,6 @@ pub struct ToolCtx { pub workspaces: Vec, pub session_dir: PathBuf, pub memory_dir: PathBuf, - pub _download_dir: PathBuf, pub worktrees_dir: PathBuf, pub dir_cache: std::sync::Arc>, pub origin: crate::app::state::types::Origin, @@ -87,7 +86,6 @@ pub struct ToolCtxBuilder { pub workspaces: Vec, pub session_dir: PathBuf, pub memory_dir: PathBuf, - pub download_dir: PathBuf, pub worktrees_dir: PathBuf, pub dir_cache: std::sync::Arc>, pub origin: crate::app::state::types::Origin, @@ -103,7 +101,6 @@ impl Default for ToolCtxBuilder { workspaces: Vec::new(), session_dir: PathBuf::new(), memory_dir: PathBuf::new(), - download_dir: PathBuf::new(), worktrees_dir: PathBuf::new(), dir_cache: std::sync::Arc::new(tokio::sync::RwLock::new(super::app::state::misc::DirCache::new())), origin: crate::app::state::types::Origin::Main, @@ -122,9 +119,6 @@ impl ToolCtxBuilder { pub fn workspaces(mut self, v: Vec) -> Self { self.workspaces = v; self } /// Set the origin (main process vs. daemon-attached). pub fn origin(mut self, v: crate::app::state::types::Origin) -> Self { self.origin = v; self } - /// Set the lsp_manager. - #[allow(dead_code)] - pub fn lsp_manager(mut self, v: Arc>) -> Self { self.lsp_manager = v; self } /// Set the workflow-level findings sharing Arc (for subagent-to-subagent /// communication within a workflow run). pub fn workflow_findings(mut self, v: Option>>>) -> Self { self.workflow_findings = v; self } @@ -134,7 +128,6 @@ impl ToolCtxBuilder { workspaces: self.workspaces, session_dir: self.session_dir, memory_dir: self.memory_dir, - _download_dir: self.download_dir, worktrees_dir: self.worktrees_dir, dir_cache: self.dir_cache, origin: self.origin, @@ -228,7 +221,6 @@ pub fn tool_defs(tools: &[Box]) -> Vec Result { - let _parts: Vec<&str> = rel.splitn(2, '/').collect(); let (ws_idx, path) = if rel.starts_with('[') { let close = rel.find(']').ok_or_else(|| anyhow::anyhow!("invalid workspace prefix"))?; let idx: usize = rel[1..close].parse().map_err(|_| anyhow::anyhow!("invalid workspace index"))?; diff --git a/src/tool/plan.rs b/src/tool/plan.rs index 82bd4c3..035096f 100644 --- a/src/tool/plan.rs +++ b/src/tool/plan.rs @@ -39,10 +39,10 @@ impl Tool for PlanEnter { /// /// Return: fixed acknowledgement string on success; error if either arg is missing. fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { - let _plan = args.get("plan") + let _ = args.get("plan") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: plan"))?; - let _sign_off = args.get("sign_off") + let _ = args.get("sign_off") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: sign_off"))?; Ok("plan recorded".to_string()) @@ -78,7 +78,7 @@ impl Tool for PlanReady { /// /// Return: fixed "ready to execute" string on success; error if `confirmation` is missing. fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { - let _confirmation = args.get("confirmation") + let _ = args.get("confirmation") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: confirmation"))?; Ok("ready to execute".to_string()) diff --git a/src/tool/shell.rs b/src/tool/shell.rs index f272088..3936f2d 100644 --- a/src/tool/shell.rs +++ b/src/tool/shell.rs @@ -63,8 +63,7 @@ impl Tool for Bash { .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: command"))? .to_string(); - let _description = args.get("description").and_then(|v| v.as_str()).unwrap_or(""); - let timeout_ms = args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(120000).min(600000); + let timeout_ms = args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(120_000).min(600_000); // Only gate destructive git operations; credential reads are allowed // locally since the AI needs access, and the real threat is committing // secrets to a public repo (handled by git pre-commit hooks / user). @@ -100,9 +99,8 @@ impl Tool for Bash { } else { format!("{}\n\nExit code: 0 ({:.2}s)", trimmed, elapsed) }); - } else { - return Ok(format!("{}\n\nExit code: {} ({:.2}s)", trimmed, status.code().unwrap_or(-1), elapsed)); } + return Ok(format!("{}\n\nExit code: {} ({:.2}s)", trimmed, status.code().unwrap_or(-1), elapsed)); } Ok(None) => { if start.elapsed() > timeout { diff --git a/src/view/mod.rs b/src/view/mod.rs index 1e809fc..ffe49ca 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -656,8 +656,8 @@ fn render_overlay( }) .unwrap_or((0, 0, 0, 0)); let elapsed_ms = chrono::Utc::now().timestamp_millis().saturating_sub(session_start); - let hours = elapsed_ms / 3600000; - let minutes = (elapsed_ms % 3600000) / 60000; + let hours = elapsed_ms / 3_600_000; + let minutes = (elapsed_ms % 3_600_000) / 60_000; let seconds = (elapsed_ms % 60000) / 1000; let total_tokens = tokens_in.saturating_add(tokens_out); let self_learning_total = review_tokens; diff --git a/src/view/status.rs b/src/view/status.rs index 7b2df11..5f5c571 100644 --- a/src/view/status.rs +++ b/src/view/status.rs @@ -25,7 +25,7 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state: let spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; // ── Agent status badge ──────────────────────────────────────────────── - let (status_text, status_bg, status_fg) = if state.turn_in_flight() { + let (status_text, badge_bg, status_fg) = if state.turn_in_flight() { let f = spinner_frames[(state.misc.tick_count as usize / 2) % spinner_frames.len()]; (format!(" {} PROG ", f), Theme::MODE_YOLO, Theme::BG) } else if state.misc.api_connected { @@ -38,7 +38,7 @@ pub fn draw_status_bar(frame: &mut Frame, area: Rect, state: &crate::app::state: status_text, Style::default() .fg(status_fg) - .bg(status_bg) + .bg(badge_bg) .add_modifier(Modifier::BOLD), );