Enable additional pedantic clippy lints and fix violations

Enables cast_possible_truncation, cast_sign_loss, items_after_statements,
needless_pass_by_value, return_self_not_must_use, uninlined_format_args,
unreadable_literal, and unnested_or_patterns. Keeps doc_markdown disabled.

Replaces unsafe `as` casts with try_from().unwrap() throughout, using
#[allow] only for f64-to-integer casts which have no try_from equivalent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-28 14:31:11 -04:00
parent 1dc0ebce52
commit 2f4fc23f3d
No known key found for this signature in database
68 changed files with 256 additions and 234 deletions

View file

@ -88,17 +88,9 @@ too_many_arguments = "allow"
too_many_lines = "allow"
used_underscore_binding = "allow"
if_not_else = "allow"
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
doc_markdown = "allow"
items_after_statements = "allow"
needless_pass_by_value = "allow"
return_self_not_must_use = "allow"
uninlined_format_args = "allow"
unreadable_literal = "allow"
unnested_or_patterns = "allow"
# Disallowed restriction lints
print_stdout = "warn"
print_stderr = "warn"

View file

@ -38,7 +38,7 @@ pub trait AgentProfile: Send + Sync {
fn context_window_size(&self) -> usize {
Catalog::builtin()
.get(self.model())
.map(|m| m.context_window() as usize)
.map(|m| usize::try_from(m.context_window()).unwrap())
.unwrap_or(200_000)
}

View file

@ -110,8 +110,7 @@ fn tool_category(name: &str) -> &'static str {
fn is_auto_approved(level: PermissionLevel, category: &str) -> bool {
matches!(
(level, category),
(_, "read")
| (_, "subagent")
(_, "read" | "subagent")
| (PermissionLevel::ReadWrite | PermissionLevel::Full, "write")
| (PermissionLevel::Full, "shell")
)

View file

@ -6,12 +6,12 @@ use fabro_mcp::connection_manager::{McpConnectionManager, call_result_to_string}
use crate::tool_registry::RegisteredTool;
/// Create `RegisteredTool` instances for every tool exposed by connected MCP servers.
pub fn make_mcp_tools(manager: Arc<McpConnectionManager>) -> Vec<RegisteredTool> {
pub fn make_mcp_tools(manager: &Arc<McpConnectionManager>) -> Vec<RegisteredTool> {
manager
.all_tools()
.iter()
.map(|(qualified_name, info)| {
let mgr = Arc::clone(&manager);
let mgr = Arc::clone(manager);
let name = qualified_name.clone();
let tool_timeout = std::time::Duration::from_secs(120);
@ -67,7 +67,7 @@ mod tests {
let mut mgr = McpConnectionManager::new();
mgr.start_servers(&[config]).await;
let tools = make_mcp_tools(Arc::new(mgr));
let tools = make_mcp_tools(&Arc::new(mgr));
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].definition.name, "mcp__test_echo__echo");
assert_eq!(tools[0].definition.description, "Echo back the message");
@ -79,7 +79,7 @@ mod tests {
let mut mgr = McpConnectionManager::new();
mgr.start_servers(&[config]).await;
let tools = make_mcp_tools(Arc::new(mgr));
let tools = make_mcp_tools(&Arc::new(mgr));
let tool = &tools[0];
use crate::sandbox::Sandbox;

View file

@ -165,7 +165,7 @@ impl Session {
}
let manager = Arc::new(manager);
let mcp_tools = mcp_integration::make_mcp_tools(manager);
let mcp_tools = mcp_integration::make_mcp_tools(&manager);
if let Some(profile) = Arc::get_mut(&mut self.provider_profile) {
for tool in mcp_tools {
profile.tool_registry_mut().register(tool);
@ -588,6 +588,8 @@ impl Session {
}
async fn run_single_input(&mut self, input: &str) -> Result<(), AgentError> {
const STREAM_CONSUME_RETRIES: usize = 3;
if self.state == SessionState::Closed {
return Err(AgentError::SessionClosed);
}
@ -700,7 +702,6 @@ impl Session {
// Consume the stream, retrying up to 3 times if the provider
// closes the stream without sending a Finish event. If visible
// output was already emitted, clear it before replaying the turn.
const STREAM_CONSUME_RETRIES: usize = 3;
let mut response = None;
for stream_attempt in 0..=STREAM_CONSUME_RETRIES {

View file

@ -346,7 +346,7 @@ pub fn make_spawn_agent_tool(
let max_turns = args
.get("max_turns")
.and_then(serde_json::Value::as_u64)
.map(|v| v as usize);
.map(|v| usize::try_from(v).unwrap());
// Note: working_dir and model require session factory changes to wire through
let mut session = session_factory();

View file

@ -190,7 +190,7 @@ async fn execute_and_emit_one_tool_with_lookup(
debug!(tool = %tc.name, hook_event = "pre_tool_use", "Calling tool hook");
let start = std::time::Instant::now();
let decision = hooks.pre_tool_use(&tc.name, &tc.arguments).await;
let elapsed = start.elapsed().as_millis() as u64;
let elapsed = u64::try_from(start.elapsed().as_millis()).unwrap();
debug!(tool = %tc.name, hook_event = "pre_tool_use", ?decision, duration_ms = elapsed, "Tool hook complete");
if let ToolHookDecision::Block { reason } = decision {

View file

@ -88,8 +88,8 @@ pub fn make_read_file_tool() -> RegisteredTool {
let offset = args.get("offset").and_then(serde_json::Value::as_u64);
let limit = args.get("limit").and_then(serde_json::Value::as_u64);
let offset_usize = offset.map(|v| v as usize);
let limit_usize = limit.map(|v| v as usize);
let offset_usize = offset.map(|v| usize::try_from(v).unwrap());
let limit_usize = limit.map(|v| usize::try_from(v).unwrap());
let content = ctx
.env
@ -279,6 +279,10 @@ pub fn make_grep_tool() -> RegisteredTool {
.and_then(serde_json::Value::as_str)
.unwrap_or(".");
let max_results = args
.get("max_results")
.and_then(serde_json::Value::as_u64)
.map(|v| usize::try_from(v).unwrap());
let options = GrepOptions {
glob_filter: args
.get("glob_filter")
@ -288,10 +292,7 @@ pub fn make_grep_tool() -> RegisteredTool {
.get("case_insensitive")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
max_results: args
.get("max_results")
.and_then(serde_json::Value::as_u64)
.map(|v| v as usize),
max_results,
};
let results = ctx.env.grep(pattern, path, &options).await?;
@ -402,7 +403,7 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool {
let depth = args
.get("depth")
.and_then(serde_json::Value::as_u64)
.map(|v| v as usize);
.map(|v| usize::try_from(v).unwrap());
let entries = ctx.env.list_directory(path, depth).await?;
let lines: Vec<String> = entries

View file

@ -1,6 +1,6 @@
//! Demo mode handlers that return static data for all API endpoints.
//! Activated per-request via the `X-Fabro-Demo: 1` header to showcase the UI without a real backend.
#![allow(clippy::default_trait_access)]
#![allow(clippy::default_trait_access, clippy::unreadable_literal)]
use std::sync::Arc;

View file

@ -69,7 +69,7 @@ pub fn decode_pem_env(name: &str, value: &str) -> String {
///
/// Call this once at startup before serving requests. Panics if the
/// configuration is invalid (JWT strategy but no public key, or mTLS without TLS config).
pub fn resolve_auth_mode(api_config: &ApiSettings, allowed_usernames: Vec<String>) -> AuthMode {
pub fn resolve_auth_mode(api_config: &ApiSettings, allowed_usernames: &[String]) -> AuthMode {
use fabro_config::server::ApiAuthStrategy;
if api_config.authentication_strategies.is_empty() {
@ -93,7 +93,7 @@ pub fn resolve_auth_mode(api_config: &ApiSettings, allowed_usernames: Vec<String
AuthStrategy::Jwt {
key: Arc::new(key),
validation: Arc::new(jwt_validation()),
allowed_usernames: allowed_usernames.clone(),
allowed_usernames: allowed_usernames.to_vec(),
}
}
ApiAuthStrategy::Mtls => {

View file

@ -127,7 +127,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
.as_ref()
.map(|w| w.auth.allowed_usernames.clone())
.unwrap_or_default();
let auth_mode = resolve_auth_mode(&api, allowed_usernames);
let auth_mode = resolve_auth_mode(&api, &allowed_usernames);
let client_auth = api.tls.as_ref().map(|_| client_auth_from_mode(&auth_mode));
let max_concurrent_runs = args
.max_concurrent_runs

View file

@ -90,6 +90,7 @@ fn turns_to_messages(turns: &[fabro_api_types::SessionTurn]) -> Vec<LlmMessage>
fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool, seq_at_start: u64) {
tokio::spawn(async move {
use futures_util::StreamExt;
let (event_tx, model_id, model_provider, system_prompt, messages, generation_seq) = {
let store = store.read().expect("session store lock poisoned");
let Some(session) = store.get(&session_id) else {
@ -156,7 +157,6 @@ fn spawn_generation(store: SessionStore, session_id: uuid::Uuid, dry_run: bool,
}
};
use futures_util::StreamExt;
let mut stream_result = stream_result;
let mut full_text = String::new();
while let Some(event) = stream_result.next().await {
@ -321,6 +321,7 @@ pub async fn stream_session_events(
State(state): State<Arc<AppState>>,
Path(id): Path<uuid::Uuid>,
) -> Response {
use tokio_stream::StreamExt;
let rx = {
let store = state.sessions.read().expect("session store lock poisoned");
match store.get(&id) {
@ -329,8 +330,6 @@ pub async fn stream_session_events(
}
};
use tokio_stream::StreamExt;
let stream = BroadcastStream::new(rx).filter_map(|result| match result {
Ok(event) => {
let sse: Option<Event> = match event {

View file

@ -12,6 +12,7 @@ use fabro_config::server::TlsSettings;
use crate::jwt_auth::PeerCertificates;
/// How client certificates should be verified.
#[derive(Clone, Copy)]
pub enum ClientAuth {
/// No client certificates requested (TLS encryption only).
None,

View file

@ -24,6 +24,14 @@ async fn list_from(
args: PrListArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
) -> Result<()> {
struct PrRow {
run_id: String,
number: u64,
state: String,
title: String,
url: String,
}
let creds = github_app.context(
"GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id",
)?;
@ -45,14 +53,6 @@ async fn list_from(
return Ok(());
}
struct PrRow {
run_id: String,
number: u64,
state: String,
title: String,
url: String,
}
let futures: Vec<_> = entries
.iter()
.map(|(run_id, record)| {

View file

@ -99,10 +99,7 @@ fn build_live_diff_cmd(base_sha: &str, stat: bool, shortstat: bool) -> String {
|_| format!("'{}'", base_sha.replace('\'', "'\\''")),
|q| q.to_string(),
);
format!(
"{} add -N . && {} diff{flags} {quoted_sha}",
GIT_REMOTE, GIT_REMOTE
)
format!("{GIT_REMOTE} add -N . && {GIT_REMOTE} diff{flags} {quoted_sha}")
}
fn colorize_diff_line(line: &str) -> String {

View file

@ -28,7 +28,7 @@ pub(crate) fn run(args: &ForkArgs, styles: &Styles) -> Result<()> {
.transpose()?;
let new_run_id = fork(
&store,
ForkRunInput {
&ForkRunInput {
source_run_id: run_id.clone(),
target,
push: !args.no_push,

View file

@ -11,7 +11,7 @@ use tracing::{debug, info};
use crate::args::LogsArgs;
use crate::cli_config::load_cli_settings;
pub(crate) fn run(args: LogsArgs, styles: &Styles) -> Result<()> {
pub(crate) fn run(args: &LogsArgs, styles: &Styles) -> Result<()> {
let cli_config = load_cli_settings(None)?;
let base = runs_base(&cli_config.storage_dir());
let run = resolve_run(&base, &args.run)?;
@ -241,9 +241,10 @@ pub(crate) fn format_event_pretty(line: &str, styles: &Styles) -> Option<String>
lines.push(format!(
"{}{}",
pad,
styles
.dim
.apply_to(format!("Tokens: {}", format_tokens(total as u64)))
styles.dim.apply_to(format!(
"Tokens: {}",
format_tokens(u64::try_from(total).unwrap())
))
));
}
if let Some(cache_read) = usage
@ -259,8 +260,8 @@ pub(crate) fn format_event_pretty(line: &str, styles: &Styles) -> Option<String>
pad,
styles.dim.apply_to(format!(
"Cache: {} read, {} write",
format_tokens(cache_read as u64),
format_tokens(cache_write as u64)
format_tokens(u64::try_from(cache_read).unwrap()),
format_tokens(u64::try_from(cache_write).unwrap())
))
));
}
@ -274,7 +275,7 @@ pub(crate) fn format_event_pretty(line: &str, styles: &Styles) -> Option<String>
pad,
styles.dim.apply_to(format!(
"Reasoning: {} tokens",
format_tokens(reasoning as u64)
format_tokens(u64::try_from(reasoning).unwrap())
))
));
}

View file

@ -66,7 +66,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
RunCommands::Diff(args) => diff::run(args).await,
RunCommands::Logs(args) => {
let styles = Styles::detect_stdout();
logs::run(args, &styles)
logs::run(&args, &styles)
}
RunCommands::Resume(args) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
@ -87,7 +87,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
}
RunCommands::Wait(args) => {
let styles = Styles::detect_stderr();
wait::run(args, &styles)
wait::run(&args, &styles)
}
}
}

View file

@ -28,7 +28,7 @@ pub(crate) fn run(args: &RewindArgs, styles: &Styles) -> Result<()> {
rewind(
&store,
RewindInput {
&RewindInput {
run_id: run_id.clone(),
target,
push: !args.no_push,

View file

@ -69,7 +69,7 @@ pub(crate) fn format_duration_short(d: Duration) -> String {
if secs >= 60 {
format!("{}m{:02}s", secs / 60, secs % 60)
} else if d.as_millis() >= 1000 {
format!("{}s", secs)
format!("{secs}s")
} else {
format!("{}ms", d.as_millis())
}
@ -83,7 +83,9 @@ fn terminal_hyperlink(url: &str, text: &str) -> String {
/// Format a number as an integer if whole, one decimal otherwise.
fn format_number(n: f64) -> String {
if (n - n.round()).abs() < f64::EPSILON {
format!("{}", n as i64)
#[allow(clippy::cast_possible_truncation)] // f64-to-integer: intentional rounding
let i = n as i64;
format!("{i}")
} else {
format!("{n:.1}")
}
@ -718,7 +720,7 @@ impl ProgressUI {
}
}
"SetupStarted" => {
let count = u64_field("command_count") as usize;
let count = usize::try_from(u64_field("command_count")).unwrap();
self.on_setup_started(count);
}
"SetupCompleted" => {
@ -972,7 +974,7 @@ impl ProgressUI {
}
"DevcontainerLifecycleStarted" => {
let phase = str_field("phase").unwrap_or("?");
let command_count = u64_field("command_count") as usize;
let command_count = usize::try_from(u64_field("command_count")).unwrap();
self.devcontainer_command_count = command_count;
match &self.renderer {
ProgressRenderer::Tty(tty) => {
@ -1478,6 +1480,8 @@ impl ProgressUI {
..
} if self.verbose => {
let yellow = Style::new().yellow();
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
// f64-to-integer: delay is non-negative and fits in u64
let delay_ms = (*delay_secs * 1000.0) as u64;
let dur = format_duration_ms(delay_ms);
self.insert_info_line_for_stage(

View file

@ -12,7 +12,7 @@ use crate::args::WaitArgs;
use crate::cli_config::load_cli_settings;
use crate::shared::format_duration_ms;
pub(crate) fn run(args: WaitArgs, styles: &Styles) -> Result<()> {
pub(crate) fn run(args: &WaitArgs, styles: &Styles) -> Result<()> {
let cli_config = load_cli_settings(None)?;
let base = runs_base(&cli_config.storage_dir());
let run_info = resolve_run(&base, &args.run)?;

View file

@ -78,7 +78,9 @@ pub(crate) fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
None => match run.start_time_dt {
Some(start) => {
let elapsed = now.signed_duration_since(start);
format_duration_ms(elapsed.num_milliseconds().max(0) as u64)
format_duration_ms(
u64::try_from(elapsed.num_milliseconds().max(0)).unwrap(),
)
}
None => "-".to_string(),
},

View file

@ -22,11 +22,6 @@ pub(super) fn df_command(args: &DfArgs) -> Result<()> {
}
fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Path) -> Result<()> {
let runs = scan_runs(runs_base)?;
let mut active_count = 0u64;
let mut total_run_size = 0u64;
let mut reclaimable_run_size = 0u64;
struct RunSizeInfo {
run_id: String,
workflow_name: String,
@ -35,6 +30,11 @@ fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Path) -
size: u64,
}
let runs = scan_runs(runs_base)?;
let mut active_count = 0u64;
let mut total_run_size = 0u64;
let mut reclaimable_run_size = 0u64;
let mut run_details = Vec::new();
for run in &runs {
let size = dir_size(&run.path);
@ -96,7 +96,11 @@ fn df_from(args: &DfArgs, data_dir: &Path, runs_base: &Path, logs_base: &Path) -
}
let run_reclaim_pct = if total_run_size > 0 {
(reclaimable_run_size as f64 / total_run_size as f64 * 100.0) as u64
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
// f64-to-integer: percentage is 0-100
{
(reclaimable_run_size as f64 / total_run_size as f64 * 100.0) as u64
}
} else {
0
};

View file

@ -35,7 +35,7 @@ async fn main() {
let raw_args: Vec<String> = std::env::args().collect();
let (command_name, result) = main_inner().await;
let duration_ms = start.elapsed().as_millis() as u64;
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap();
let is_error = result.is_err();
let command = sanitize::sanitize_command(&raw_args, &command_name);

View file

@ -183,6 +183,7 @@ impl Combine for FabroConfig {
}
impl FabroConfig {
#[must_use]
pub fn combine(self, other: Self) -> Self {
Combine::combine(self, other)
}

View file

@ -46,6 +46,7 @@ impl Context {
/// Deep copy for parallel branch isolation.
/// `.clone()` shares state (Arc clone); `.fork()` creates an independent copy.
#[must_use]
pub fn fork(&self) -> Self {
Self {
values: Arc::new(RwLock::new(self.snapshot())),
@ -64,7 +65,8 @@ impl Context {
pub fn node_visit_count(&self) -> usize {
self.get("internal.node_visit_count")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize
.map(|v| usize::try_from(v).unwrap())
.unwrap_or(0)
}
}

View file

@ -51,21 +51,25 @@ impl<G: Graph + 'static> ExecutorBuilder<G> {
}
}
#[must_use]
pub fn lifecycle(mut self, lifecycle: Box<dyn RunLifecycle<G>>) -> Self {
self.lifecycle = Some(lifecycle);
self
}
#[must_use]
pub fn cancel_token(mut self, token: Arc<AtomicBool>) -> Self {
self.options.cancel_token = Some(token);
self
}
#[must_use]
pub fn stall_token(mut self, token: CancellationToken) -> Self {
self.options.stall_token = Some(token);
self
}
#[must_use]
pub fn max_node_visits(mut self, limit: usize) -> Self {
self.options.max_node_visits = Some(limit);
self

View file

@ -29,7 +29,7 @@ pub(crate) fn generate(
}
if let Some(user) = remote_user {
sections.push(format!("USER {}", user));
sections.push(format!("USER {user}"));
}
let mut result = sections.join("\n\n");

View file

@ -545,7 +545,7 @@ impl DevcontainerResolver {
ports
.iter()
.filter_map(|p| match p {
serde_json::Value::Number(n) => n.as_u64().map(|n| n as u16),
serde_json::Value::Number(n) => n.as_u64().map(|n| u16::try_from(n).unwrap()),
serde_json::Value::String(s) => {
let s = s.split('/').next().unwrap_or(s); // strip protocol
if let Some((_host, container)) = s.split_once(':') {

View file

@ -16,16 +16,16 @@ pub enum FileMode {
impl FileMode {
fn as_i32(self) -> i32 {
match self {
Self::Blob => 0o100644,
Self::BlobExecutable => 0o100755,
Self::Tree => 0o040000,
Self::Blob => 0o100_644,
Self::BlobExecutable => 0o100_755,
Self::Tree => 0o040_000,
}
}
fn from_i32(mode: i32) -> Self {
match mode {
0o100755 => Self::BlobExecutable,
0o040000 => Self::Tree,
0o100_755 => Self::BlobExecutable,
0o040_000 => Self::Tree,
_ => Self::Blob,
}
}

View file

@ -7,7 +7,7 @@ pub struct Trailer<'a> {
}
/// Append a trailer to a commit message, inserting a blank-line separator if needed.
pub fn append(message: &str, trailer: Trailer<'_>) -> String {
pub fn append(message: &str, trailer: &Trailer<'_>) -> String {
let trailer_line = format!("{}: {}", trailer.key, trailer.value);
let trimmed = message.trim_end();
@ -93,7 +93,7 @@ mod tests {
fn append_to_simple_message() {
let result = append(
"Initial commit",
Trailer {
&Trailer {
key: "My-Checkpoint",
value: "abc123",
},
@ -106,7 +106,7 @@ mod tests {
let msg = "Initial commit\n\nSigned-off-by: Alice <alice@example.com>\n";
let result = append(
msg,
Trailer {
&Trailer {
key: "My-Checkpoint",
value: "abc123",
},
@ -122,7 +122,7 @@ mod tests {
let msg = "Initial commit\n\nThis is a longer description of the change.\n";
let result = append(
msg,
Trailer {
&Trailer {
key: "My-Checkpoint",
value: "abc123",
},

View file

@ -290,6 +290,13 @@ pub async fn create_pull_request(
body: &str,
draft: bool,
) -> Result<CreatedPullRequest, String> {
#[derive(Deserialize)]
struct PullRequestResponse {
html_url: String,
number: u64,
node_id: String,
}
let jwt = sign_app_jwt(&creds.app_id, &creds.private_key_pem)?;
let client = reqwest::Client::new();
@ -329,8 +336,7 @@ pub async fn create_pull_request(
}
401 | 403 => {
return Err(format!(
"Authentication failed creating pull request ({})",
status
"Authentication failed creating pull request ({status})"
));
}
_ => {
@ -341,13 +347,6 @@ pub async fn create_pull_request(
}
}
#[derive(Deserialize)]
struct PullRequestResponse {
html_url: String,
number: u64,
node_id: String,
}
let pr: PullRequestResponse = resp
.json()
.await

View file

@ -121,7 +121,7 @@ impl HookExecutorImpl {
work_dir: Option<&Path>,
) -> HookDecision {
let context_json = serde_json::to_string(context).unwrap_or_default();
let timeout_ms = definition.timeout().as_millis() as u64;
let timeout_ms = u64::try_from(definition.timeout().as_millis()).unwrap();
let mut env_vars = HashMap::new();
env_vars.insert("FABRO_EVENT".to_string(), context.event.to_string());
@ -589,7 +589,7 @@ impl HookExecutor for HookExecutorImpl {
},
};
let duration_ms = start.elapsed().as_millis() as u64;
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap();
HookResult {
hook_name: definition.name.clone(),
decision,

View file

@ -103,8 +103,8 @@ impl HookDecision {
match (&self, &other) {
(Self::Block { .. }, _) => self,
(_, Self::Block { .. }) => other,
(Self::Skip { .. }, _) | (Self::Override { .. }, _) => self,
(_, Self::Skip { .. }) | (_, Self::Override { .. }) => other,
(Self::Skip { .. } | Self::Override { .. }, _) => self,
(_, Self::Skip { .. } | Self::Override { .. }) => other,
_ => Self::Proceed,
}
}

View file

@ -113,6 +113,7 @@ fn format_cost(cost: Option<f64>) -> String {
fn format_speed(tps: Option<f64>) -> String {
match tps {
None => "-".to_string(),
#[allow(clippy::cast_possible_truncation)] // f64-to-integer: fractional loss is fine
Some(t) => format!("{} tok/s", t as i64),
}
}

View file

@ -70,9 +70,9 @@ impl McpClient {
let mut header_map = HeaderMap::new();
for (key, value) in headers {
let name = HeaderName::from_bytes(key.as_bytes())
.map_err(|e| anyhow!("invalid header name '{}': {}", key, e))?;
.map_err(|e| anyhow!("invalid header name '{key}': {e}"))?;
let val = HeaderValue::from_str(value)
.map_err(|e| anyhow!("invalid header value for '{}': {}", key, e))?;
.map_err(|e| anyhow!("invalid header value for '{key}': {e}"))?;
header_map.insert(name, val);
}
builder = builder.default_headers(header_map);
@ -207,8 +207,7 @@ impl McpClient {
serde_json::Value::Null => None,
other => {
return Err(anyhow!(
"MCP tool arguments must be a JSON object, got {}",
other
"MCP tool arguments must be a JSON object, got {other}"
));
}
};

View file

@ -906,7 +906,7 @@ impl Sandbox for DaytonaSandbox {
name: f.name,
is_dir: f.is_dir,
size: if f.size > 0 {
Some(f.size as u64)
Some(u64::try_from(f.size).unwrap())
} else {
None
},
@ -937,7 +937,7 @@ impl Sandbox for DaytonaSandbox {
.map_err(|e| format!("Failed to get process service: {e}"))?;
tracing::info!(
elapsed_ms = start.elapsed().as_millis() as u64,
elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap(),
"exec_command: process service acquired, starting select"
);
@ -976,7 +976,7 @@ impl Sandbox for DaytonaSandbox {
let result = tokio::select! {
res = exec_future => {
tracing::info!(
elapsed_ms = start.elapsed().as_millis() as u64,
elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap(),
ok = res.is_ok(),
"exec_command: HTTP response received"
);
@ -984,7 +984,7 @@ impl Sandbox for DaytonaSandbox {
}
() = time::sleep(timeout_duration) => {
tracing::info!(
elapsed_ms = start.elapsed().as_millis() as u64,
elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap(),
timeout_ms,
"exec_command: client-side timeout fired"
);
@ -993,12 +993,12 @@ impl Sandbox for DaytonaSandbox {
stderr: "Command timed out locally".to_string(),
exit_code: -1,
timed_out: true,
duration_ms: start.elapsed().as_millis() as u64,
duration_ms: u64::try_from(start.elapsed().as_millis()).unwrap(),
});
}
() = token.cancelled() => {
tracing::info!(
elapsed_ms = start.elapsed().as_millis() as u64,
elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap(),
"exec_command: cancelled via token"
);
return Ok(ExecResult {
@ -1006,12 +1006,12 @@ impl Sandbox for DaytonaSandbox {
stderr: "Command cancelled".to_string(),
exit_code: -1,
timed_out: true,
duration_ms: start.elapsed().as_millis() as u64,
duration_ms: u64::try_from(start.elapsed().as_millis()).unwrap(),
});
}
};
let duration_ms = start.elapsed().as_millis() as u64;
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap();
// The Daytona SDK returns combined output in `result` field.
// Separate stderr isn't available in the simple execute_command API.

View file

@ -189,7 +189,7 @@ impl DockerSandbox {
.await
.map_err(|e| format!("Failed to inspect exec: {e}"))?;
let exit_code = inspect.exit_code.unwrap_or(-1) as i32;
let exit_code = i32::try_from(inspect.exit_code.unwrap_or(-1)).unwrap();
Ok((stdout, stderr, exit_code))
}

View file

@ -118,9 +118,6 @@ impl Sandbox for LocalSandbox {
path: &str,
depth: Option<usize>,
) -> Result<Vec<DirEntry>, String> {
let full_path = self.resolve_path(path);
let max_depth = depth.unwrap_or(1);
fn list_recursive(
base: &std::path::Path,
prefix: &str,
@ -160,6 +157,8 @@ impl Sandbox for LocalSandbox {
Ok(())
}
let full_path = self.resolve_path(path);
let max_depth = depth.unwrap_or(1);
let mut entries = Vec::new();
list_recursive(&full_path, "", 0, max_depth, &mut entries)?;
Ok(entries)

View file

@ -295,7 +295,7 @@ impl RunStore for SlateRunStore {
let mut iter = self.inner.db.scan_prefix(prefix.as_bytes()).await?;
let mut visits = BTreeSet::new();
while let Some(entry) = iter.next().await? {
let key = key_to_string(entry.key)?;
let key = key_to_string(&entry.key)?;
if let Some((current_node_id, visit, _)) = keys::parse_node_key(&key) {
if current_node_id == node_id {
visits.insert(visit);
@ -400,7 +400,7 @@ impl RunStore for SlateRunStore {
let mut iter = self.inner.db.scan_prefix(prefix.as_bytes()).await?;
let mut assets = Vec::new();
while let Some(entry) = iter.next().await? {
let key = key_to_string(entry.key)?;
let key = key_to_string(&entry.key)?;
if let Some(asset) = key.strip_prefix(&prefix) {
assets.push(asset.to_string());
}
@ -417,7 +417,7 @@ impl RunStore for SlateRunStore {
let mut iter = self.inner.db.scan_prefix(b"nodes/").await?;
let mut visits = BTreeSet::new();
while let Some(entry) = iter.next().await? {
let key = key_to_string(entry.key)?;
let key = key_to_string(&entry.key)?;
if let Some((node_id, visit, _)) = keys::parse_node_key(&key) {
visits.insert((node_id, visit));
}
@ -497,7 +497,7 @@ where
let mut iter = db.scan_prefix(prefix.as_bytes()).await?;
let mut max_seq = 0;
while let Some(entry) = iter.next().await? {
let key = key_to_string(entry.key)?;
let key = key_to_string(&entry.key)?;
if let Some(seq) = parse(&key) {
max_seq = max_seq.max(seq);
}
@ -512,7 +512,7 @@ where
let mut iter = db.scan_prefix(keys::EVENTS_PREFIX.as_bytes()).await?;
let mut events = Vec::new();
while let Some(entry) = iter.next().await? {
let key = key_to_string(entry.key)?;
let key = key_to_string(&entry.key)?;
let Some(seq) = keys::parse_event_seq(&key) else {
continue;
};
@ -535,7 +535,7 @@ where
let mut iter = db.scan_prefix(keys::CHECKPOINTS_PREFIX.as_bytes()).await?;
let mut checkpoints = Vec::new();
while let Some(entry) = iter.next().await? {
let key = key_to_string(entry.key)?;
let key = key_to_string(&entry.key)?;
let Some(seq) = keys::parse_checkpoint_seq(&key) else {
continue;
};
@ -545,7 +545,7 @@ where
Ok(checkpoints)
}
fn key_to_string(key: Bytes) -> Result<String> {
fn key_to_string(key: &Bytes) -> Result<String> {
String::from_utf8(key.to_vec())
.map_err(|err| StoreError::Other(format!("stored key is not valid UTF-8: {err}")))
}

View file

@ -46,7 +46,7 @@ pub fn compute_cli_id() -> Result<String> {
.context("no MAC address found")?;
let digest = md5::compute(mac.bytes());
Ok(format!("{:x}", digest))
Ok(format!("{digest:x}"))
}
#[cfg(test)]

View file

@ -3,6 +3,7 @@ use std::time::{Duration, Instant};
use crate::event::Track;
#[derive(Clone, Copy)]
pub(crate) struct BufferPolicy {
pub count_threshold: usize,
pub time_threshold: Duration,
@ -18,7 +19,7 @@ impl Default for BufferPolicy {
}
pub(crate) fn consumer_loop(
rx: Receiver<Track>,
rx: &Receiver<Track>,
config: BufferPolicy,
mid_flush: impl Fn(&[Track]),
final_flush: impl Fn(&[Track]),
@ -92,7 +93,7 @@ mod tests {
drop(tx);
consumer_loop(
rx,
&rx,
BufferPolicy {
count_threshold: 2,
time_threshold: Duration::from_secs(60),
@ -127,7 +128,7 @@ mod tests {
drop(tx);
consumer_loop(
rx,
&rx,
BufferPolicy {
count_threshold: 2,
time_threshold: Duration::from_secs(60),
@ -157,7 +158,7 @@ mod tests {
// Don't drop yet — let time threshold fire
let handle = std::thread::spawn(move || {
consumer_loop(
rx,
&rx,
BufferPolicy {
count_threshold: 100, // won't trigger
time_threshold: Duration::from_millis(50),
@ -196,7 +197,7 @@ mod tests {
drop(tx); // disconnect immediately, below count threshold
consumer_loop(
rx,
&rx,
BufferPolicy {
count_threshold: 100, // won't trigger
time_threshold: Duration::from_secs(60),

View file

@ -81,7 +81,7 @@ fn init_inner(level: TelemetryLevel, anonymous_id: String) {
.name("telemetry".to_string())
.spawn(move || {
buffer::consumer_loop(
rx,
&rx,
buffer::BufferPolicy::default(),
|tracks| {
if let Err(err) = sender::upload_blocking(tracks) {

View file

@ -93,11 +93,11 @@ fn report_panic(info: &PanicHookInfo<'_>) {
}
let event = build_event(&message);
spawn_panic_sender(event);
spawn_panic_sender(&event);
}
/// Serialize the Sentry event to a temp file and spawn `fabro __send_panic <path>`.
fn spawn_panic_sender(event: Event<'static>) {
fn spawn_panic_sender(event: &Event<'static>) {
let Ok(json) = serde_json::to_vec(&event) else {
return;
};
@ -174,8 +174,7 @@ mod tests {
#[test]
fn send_panic_noops_without_dsn() {
// SENTRY_DSN is not set at compile time in tests, so this should error.
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(capture(Path::new("/nonexistent")));
let result = capture(Path::new("/nonexistent"));
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("SENTRY_DSN not set"));

View file

@ -57,7 +57,7 @@ fn normalize_issue(node: &Value) -> Result<Issue, String> {
.map(std::string::ToString::to_string);
let priority = match node["priority"].as_i64() {
Some(0) | None => None,
Some(n) => Some(n as i32),
Some(n) => Some(i32::try_from(n).unwrap()),
};
let state = node["state"]["name"]
.as_str()

View file

@ -3,6 +3,7 @@ use std::hash::Hash;
use std::path::PathBuf;
pub trait Combine {
#[must_use]
fn combine(self, other: Self) -> Self;
}

View file

@ -72,7 +72,7 @@ impl AttrValue {
pub fn is_llm_handler_type(handler_type: Option<&str>) -> bool {
matches!(
handler_type,
Some("agent") | Some("agent_loop") | Some("prompt") | Some("one_shot")
Some("agent" | "agent_loop" | "prompt" | "one_shot")
)
}
@ -428,6 +428,7 @@ impl Graph {
/// Graph-level `loop_restart_signature_limit` (default 3).
/// When the same failure signature repeats this many times, the pipeline aborts.
pub fn loop_restart_signature_limit(&self) -> usize {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // filtered >= 1 above
self.attrs
.get("loop_restart_signature_limit")
.and_then(AttrValue::as_i64)

View file

@ -180,8 +180,7 @@ impl HookDefinition {
format!("{event_str}:{short}")
}
Some(HookType::Http { ref url, .. }) => format!("{event_str}:{url}"),
Some(HookType::Prompt { ref prompt, .. })
| Some(HookType::Agent { ref prompt, .. }) => {
Some(HookType::Prompt { ref prompt, .. } | HookType::Agent { ref prompt, .. }) => {
let short = &prompt[..prompt.floor_char_boundary(20)];
format!("{event_str}:{short}")
}

View file

@ -39,16 +39,16 @@ impl RunStatus {
matches!(
(self, to),
(Self::Submitted, Self::Starting)
| (Self::Starting, Self::Running)
| (Self::Starting, Self::Failed)
| (Self::Running, Self::Succeeded)
| (Self::Running, Self::Failed)
| (Self::Running, Self::Paused)
| (Self::Running, Self::Removing)
| (Self::Paused, Self::Running)
| (Self::Paused, Self::Failed)
| (Self::Starting | Self::Paused, Self::Running)
| (
Self::Starting | Self::Running | Self::Paused | Self::Removing,
Self::Failed
)
| (
Self::Running,
Self::Succeeded | Self::Paused | Self::Removing
)
| (Self::Paused, Self::Removing)
| (Self::Removing, Self::Failed)
)
}

View file

@ -122,7 +122,7 @@ fn main() {
// Entropy
match rule.entropy {
Some(e) => {
let _ = writeln!(code, " entropy: Some({:.1}),", e);
let _ = writeln!(code, " entropy: Some({e:.1}),");
}
None => code.push_str(" entropy: None,\n"),
}

View file

@ -66,11 +66,12 @@ impl CheckReport {
footer: Option<&str>,
max_width: Option<u16>,
) -> String {
let mut out = String::new();
let width = max_width.unwrap_or(80) as usize;
// " • " is 8 chars of prefix before detail text
const DETAIL_PREFIX_LEN: usize = 8;
let mut out = String::new();
let width = max_width.unwrap_or(80) as usize;
let show_section_headers = self.sections.len() > 1;
writeln!(out, "{}", s.bold.apply_to(&self.title)).unwrap();

View file

@ -30,50 +30,49 @@ fn should_skip_object(obj: &serde_json::Map<String, Value>) -> bool {
}
}
fn walk_replacements(
v: &Value,
seen: &mut std::collections::HashSet<String>,
repls: &mut Vec<(String, String)>,
) {
match v {
Value::Object(obj) => {
if should_skip_object(obj) {
return;
}
for (k, child) in obj {
if should_skip_field(k) {
continue;
}
walk_replacements(child, seen, repls);
}
}
Value::Array(arr) => {
for child in arr {
walk_replacements(child, seen, repls);
}
}
Value::String(s) => {
let redacted = super::redact_string(s);
if redacted != *s && seen.insert(s.clone()) {
repls.push((s.clone(), redacted));
}
}
_ => {}
}
}
/// Walk a parsed JSON value and collect (original, redacted) string pairs.
fn collect_replacements(v: &Value) -> Vec<(String, String)> {
let mut seen = std::collections::HashSet::new();
let mut repls = Vec::new();
fn walk(
v: &Value,
seen: &mut std::collections::HashSet<String>,
repls: &mut Vec<(String, String)>,
) {
match v {
Value::Object(obj) => {
if should_skip_object(obj) {
return;
}
for (k, child) in obj {
if should_skip_field(k) {
continue;
}
walk(child, seen, repls);
}
}
Value::Array(arr) => {
for child in arr {
walk(child, seen, repls);
}
}
Value::String(s) => {
let redacted = super::redact_string(s);
if redacted != *s && seen.insert(s.clone()) {
repls.push((s.clone(), redacted));
}
}
_ => {}
}
}
walk(v, &mut seen, &mut repls);
walk_replacements(v, &mut seen, &mut repls);
repls
}
/// JSON-encode a string value (with quotes), without HTML escaping.
fn json_encode_string(s: &str) -> String {
serde_json::to_string(s).unwrap_or_else(|_| format!("\"{}\"", s))
serde_json::to_string(s).unwrap_or_else(|_| format!("\"{s}\""))
}
/// Redact secrets in a single JSONL line.

View file

@ -1100,10 +1100,11 @@ fn rename_fields(event_name: &str, fields: &mut serde_json::Map<String, serde_js
/// Current time as epoch milliseconds.
fn epoch_millis() -> i64 {
std::time::SystemTime::now()
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
.as_millis();
i64::try_from(millis).unwrap()
}
/// Listener callback type for workflow run events.

View file

@ -31,7 +31,9 @@ impl NodeSpec for WorkflowNode {
}
fn max_visits(&self) -> Option<usize> {
self.0.max_visits().map(|v| v.max(0) as usize)
self.0
.max_visits()
.map(|v| usize::try_from(v.max(0)).unwrap())
}
}

View file

@ -13,7 +13,8 @@ use tokio::fs;
use super::{EngineServices, Handler};
fn timeout_ms(node: &Node) -> Option<u64> {
node.timeout().map(|d| d.as_millis() as u64)
node.timeout()
.map(|d| u64::try_from(d.as_millis()).unwrap())
}
/// Shell-escape a string using `shlex::try_quote` (POSIX-safe).
@ -107,7 +108,9 @@ impl Handler for CommandHandler {
script.to_string()
};
let timeout_ms = node.timeout().map_or(600_000, |d| d.as_millis() as u64);
let timeout_ms = node
.timeout()
.map_or(600_000, |d| u64::try_from(d.as_millis()).unwrap());
let env_vars = if services.env.is_empty() {
None
} else {

View file

@ -440,6 +440,7 @@ fn build_compact_preamble(
// Summary preamble
// ---------------------------------------------------------------------------
#[derive(Clone, Copy)]
enum SummaryDetail {
Low,
Medium,

View file

@ -114,7 +114,7 @@ pub trait Handler: Send + Sync {
}
/// Extract a human-readable message from a panic payload.
pub(crate) fn format_panic_message(payload: Box<dyn Any + Send>) -> String {
pub(crate) fn format_panic_message(payload: &Box<dyn Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
format!("handler panicked: {s}")
} else if let Some(s) = payload.downcast_ref::<String>() {

View file

@ -128,6 +128,15 @@ impl Handler for ParallelHandler {
run_dir: &Path,
services: &EngineServices,
) -> Result<Outcome, FabroError> {
// Build per-branch sandboxes (sequentially for git setup)
struct BranchSetup {
target_id: String,
branch_index: usize,
branch_context: Context,
sandbox: Arc<dyn Sandbox>,
worktree_path: Option<PathBuf>,
}
let parallel_start = Instant::now();
let branches = graph.outgoing_edges(&node.id);
if branches.is_empty() {
@ -188,15 +197,6 @@ impl Handler for ParallelHandler {
None
};
// Build per-branch sandboxes (sequentially for git setup)
struct BranchSetup {
target_id: String,
branch_index: usize,
branch_context: Context,
sandbox: Arc<dyn Sandbox>,
worktree_path: Option<PathBuf>,
}
let mut branch_setups: Vec<BranchSetup> = Vec::new();
for (branch_index, edge) in branches.iter().enumerate() {
let target_id = edge.to.clone();

View file

@ -164,7 +164,10 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
index: stage_index,
attempt: ctx.attempt as usize,
max_attempts: ctx.result.max_attempts as usize,
delay_ms: ctx.backoff_delay.map(|d| d.as_millis() as u64).unwrap_or(0),
delay_ms: ctx
.backoff_delay
.map(|d| u64::try_from(d.as_millis()).unwrap())
.unwrap_or(0),
});
}
Ok(())
@ -183,7 +186,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
}
let gv = node.inner();
let stage_index = state.stage_index;
let duration_ms = result.duration.as_millis() as u64;
let duration_ms = u64::try_from(result.duration.as_millis()).unwrap();
if outcome.status == StageStatus::Fail {
self.emitter.emit(&WorkflowRunEvent::StageFailed {
@ -286,7 +289,8 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
if state.cancelled {
return;
}
let duration_ms = self.run_start.lock().unwrap().elapsed().as_millis() as u64;
let duration_ms =
u64::try_from(self.run_start.lock().unwrap().elapsed().as_millis()).unwrap();
let artifact_count = self.artifact_store.lock().unwrap().list().len();
let last_sha = self.last_git_sha.lock().unwrap().clone();
let total_cost = {

View file

@ -77,15 +77,15 @@ pub(crate) struct WorkflowLifecycle {
impl WorkflowLifecycle {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
emitter: Arc<EventEmitter>,
emitter: &Arc<EventEmitter>,
hook_runner: Option<Arc<HookRunner>>,
sandbox: Arc<dyn Sandbox>,
sandbox: &Arc<dyn Sandbox>,
graph: Arc<GvGraph>,
run_dir: PathBuf,
run_options: Arc<RunOptions>,
run_dir: &PathBuf,
run_options: &Arc<RunOptions>,
is_resume: bool,
) -> Self {
let runtime_state = RuntimeState::new(&run_dir);
let runtime_state = RuntimeState::new(run_dir);
let restarted_from: Arc<Mutex<Option<(String, String)>>> = Arc::new(Mutex::new(None));
let loop_restart_signature_limit = graph.loop_restart_signature_limit();
let checkpoint_git_result: Arc<Mutex<Option<GitCheckpointResult>>> =
@ -110,7 +110,7 @@ impl WorkflowLifecycle {
};
let event = EventLifecycle {
emitter: Arc::clone(&emitter),
emitter: Arc::clone(emitter),
graph_name: graph.name.clone(),
run_id: run_options.run_id.clone(),
run_start: Mutex::new(Instant::now()),
@ -127,7 +127,7 @@ impl WorkflowLifecycle {
let hook = HookLifecycle {
hook_runner,
sandbox: Arc::clone(&sandbox),
sandbox: Arc::clone(sandbox),
hook_work_dir: working_directory.clone().map(PathBuf::from),
run_id: run_options.run_id.clone(),
graph_name: graph.name.clone(),
@ -139,8 +139,8 @@ impl WorkflowLifecycle {
run_dir: run_dir.clone(),
run_id: run_options.run_id.clone(),
graph: Arc::clone(&graph),
run_options: Arc::clone(&run_options),
emitter: Arc::clone(&emitter),
run_options: Arc::clone(run_options),
emitter: Arc::clone(emitter),
circuit_breaker: Arc::clone(&circuit_breaker),
checkpoint_enabled: true,
};
@ -148,22 +148,22 @@ impl WorkflowLifecycle {
let start_node_id = graph.find_start_node().map(|n| n.id.clone());
let git = GitLifecycle {
sandbox: Arc::clone(&sandbox),
sandbox: Arc::clone(sandbox),
artifact_store: Arc::clone(&artifact_store),
emitter: Arc::clone(&emitter),
emitter: Arc::clone(emitter),
run_dir: run_dir.clone(),
run_id: run_options.run_id.clone(),
run_options: Arc::clone(&run_options),
run_options: Arc::clone(run_options),
start_node_id,
checkpoint_git_result: Arc::clone(&checkpoint_git_result),
last_git_sha: Arc::clone(&last_git_sha),
};
let artifact = ArtifactLifecycle::new(
Arc::clone(&sandbox),
Arc::clone(sandbox),
Arc::clone(&artifact_store),
Some(runtime_state.artifact_values_dir()),
Arc::clone(&emitter),
Arc::clone(emitter),
runtime_state.assets_dir(),
run_options.asset_globs().to_vec(),
);

View file

@ -99,7 +99,7 @@ impl NodeHandler<WorkflowGraph> for WorkflowNodeHandler {
}))
}
Err(panic_payload) => {
let msg = format_panic_message(panic_payload);
let msg = format_panic_message(&panic_payload);
let visit = context.node_visit_count().max(1);
let panic_dir = run_dir::node_dir(&self.run_dir, &gv_node.id, visit);
let _ = std::fs::create_dir_all(&panic_dir);

View file

@ -19,7 +19,7 @@ pub struct ForkRunInput {
/// Create a new run that branches from an existing run at a specific checkpoint.
///
/// Returns the new run ID.
pub fn fork(store: &Store, input: ForkRunInput) -> Result<String> {
pub fn fork(store: &Store, input: &ForkRunInput) -> Result<String> {
let timeline = build_timeline(store, &input.source_run_id)?;
let entry = match input.target.as_ref() {
Some(target) => timeline.resolve(target)?,
@ -39,7 +39,7 @@ fn fork_from_entry(
let new_run_id = ulid::Ulid::new().to_string();
let sig = Signature::now("Fabro", "noreply@fabro.sh")?;
let new_run_branch = format!("{}{new_run_id}", RUN_BRANCH_PREFIX);
let new_run_branch = format!("{RUN_BRANCH_PREFIX}{new_run_id}");
match &entry.run_commit_sha {
Some(sha) => {
let oid =
@ -134,7 +134,7 @@ fn fork_from_entry(
.map_err(|e| anyhow::anyhow!("failed to write metadata entries: {e}"))?;
if push {
let source_run_branch = format!("{}{source_run_id}", RUN_BRANCH_PREFIX);
let source_run_branch = format!("{RUN_BRANCH_PREFIX}{source_run_id}");
let run_refspec = format!("refs/heads/{new_run_branch}:refs/heads/{new_run_branch}");
let meta_refspec = format!("refs/heads/{new_meta_branch}:refs/heads/{new_meta_branch}");
push_run_branches(
@ -246,7 +246,7 @@ mod tests {
let new_run_id = fork(
&store,
ForkRunInput {
&ForkRunInput {
source_run_id: source_run_id.to_string(),
target: Some(RewindTarget::from_str("@2").unwrap()),
push: false,

View file

@ -166,7 +166,7 @@ fn backfill_run_shas(store: &Store, run_id: &str, timeline: &mut [TimelineEntry]
return;
}
let run_branch = format!("{}{run_id}", RUN_BRANCH_PREFIX);
let run_branch = format!("{RUN_BRANCH_PREFIX}{run_id}");
let Ok(sig) = Signature::now("Fabro", "noreply@fabro.sh") else {
return;
};
@ -241,7 +241,7 @@ fn detect_parallel_interior(graph: &Graph) -> HashMap<String, String> {
interior_map
}
pub fn rewind(store: &Store, input: RewindInput) -> Result<()> {
pub fn rewind(store: &Store, input: &RewindInput) -> Result<()> {
let timeline = build_timeline(store, &input.run_id)?;
let entry = timeline.resolve(&input.target)?;
rewind_to_entry(store, &input.run_id, entry, input.push)
@ -258,7 +258,7 @@ fn rewind_to_entry(store: &Store, run_id: &str, entry: &TimelineEntry, push: boo
entry.ordinal, entry.node_name
);
let run_branch = format!("{}{run_id}", RUN_BRANCH_PREFIX);
let run_branch = format!("{RUN_BRANCH_PREFIX}{run_id}");
match &entry.run_commit_sha {
Some(sha) => {
let oid =
@ -494,7 +494,7 @@ mod tests {
rewind(
&store,
RewindInput {
&RewindInput {
run_id: "run-1".to_string(),
target: RewindTarget::Ordinal(1),
push: false,

View file

@ -148,7 +148,7 @@ fn persist_terminal_engine_failure(run_dir: &Path, error: &FabroError, duration:
run_dir,
final_status,
failure_reason,
duration.as_millis() as u64,
u64::try_from(duration.as_millis()).unwrap(),
None,
);
persist_terminal_outcome(run_dir, &conclusion, run_status, status_reason);
@ -472,7 +472,7 @@ impl RunSession {
let executed = pipeline::execute(initialized).await;
let failed = !matches!(
executed.outcome.as_ref().map(|outcome| &outcome.status),
Ok(StageStatus::Success) | Ok(StageStatus::PartialSuccess)
Ok(StageStatus::Success | StageStatus::PartialSuccess)
);
let retro_opts = RetroOptions {

View file

@ -24,6 +24,7 @@ pub trait OutcomeExt: Sized {
fn fail_classify(reason: impl Into<String>) -> Self;
fn retry_classify(reason: impl Into<String>) -> Self;
fn simulated(node_id: &str) -> Self;
#[must_use]
fn with_signature(self, sig: Option<impl Into<String>>) -> Self;
fn failure_reason(&self) -> Option<&str>;
fn failure_category(&self) -> Option<FailureCategory>;

View file

@ -85,12 +85,12 @@ pub async fn execute(init: Initialized) -> Executed {
let settings_arc = Arc::new(run_options.clone());
let lifecycle = WorkflowLifecycle::new(
Arc::clone(&emitter),
&emitter,
hook_runner.clone(),
Arc::clone(&sandbox),
&sandbox,
graph_arc,
run_options.run_dir.clone(),
settings_arc,
&run_options.run_dir,
&settings_arc,
checkpoint.is_some(),
);
@ -204,7 +204,7 @@ pub async fn execute(init: Initialized) -> Executed {
let graph_max = graph.max_node_visits();
let max_node_visits = if graph_max > 0 {
Some(graph_max as usize)
Some(usize::try_from(graph_max).unwrap())
} else if run_options.dry_run_enabled() {
Some(10)
} else {
@ -228,12 +228,15 @@ pub async fn execute(init: Initialized) -> Executed {
return;
}
let last = emitter.last_event_at();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
let now = i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis(),
)
.unwrap();
let idle_ms = now.saturating_sub(last);
if idle_ms >= stall_timeout.as_millis() as i64 {
if idle_ms >= i64::try_from(stall_timeout.as_millis()).unwrap() {
token_clone.cancel();
return;
}

View file

@ -74,7 +74,7 @@ fn format_duration_ms(ms: u64) -> String {
if secs >= 60 {
format!("{}m {}s", secs / 60, secs % 60)
} else {
format!("{}s", secs)
format!("{secs}s")
}
}

View file

@ -84,7 +84,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option<Retro> {
Err(anyhow::anyhow!("No LLM client available"))
};
let duration_ms = retro_start.elapsed().as_millis() as u64;
let duration_ms = u64::try_from(retro_start.elapsed().as_millis()).unwrap();
if let Some(ref emitter) = options.emitter {
match &narrative_result {
Ok(_) => emitter.emit(&WorkflowRunEvent::RetroCompleted { duration_ms }),