From 53e2a980bf4794d95076e237d4f72e474544f522 Mon Sep 17 00:00:00 2001 From: Fabro Date: Thu, 16 Apr 2026 13:19:26 -0400 Subject: [PATCH] checkpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚒️ Generated with [Fabro](https://fabro.sh) --- checkpoint.json | 29 +- nodes/fix_lints/diff.patch | 47606 ++++++++++++++++++++++++++++++++++ nodes/fix_lints/status.json | 6 + 3 files changed, 47626 insertions(+), 15 deletions(-) create mode 100644 nodes/fix_lints/diff.patch create mode 100644 nodes/fix_lints/status.json diff --git a/checkpoint.json b/checkpoint.json index ab1e5168a..cd8cb0cb7 100644 --- a/checkpoint.json +++ b/checkpoint.json @@ -1,22 +1,24 @@ { - "timestamp": "2026-04-16T17:19:22.309800Z", - "current_node": "fix_lints", + "timestamp": "2026-04-16T17:19:26.541168Z", + "current_node": "preflight_lint", "completed_nodes": [ "start", "toolchain", "preflight_compile", "preflight_lint", - "fix_lints" + "fix_lints", + "preflight_lint" ], "node_retries": {}, "context_values": { - "command.output": "error: use of a disallowed method `std::process::Command::new`\n --> lib/crates/fabro-server/src/serve.rs:504:21\n |\n504 | let child = std::process::Command::new(\"bun\")\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^\n |\n = note: Prefer tokio::process::Command on Tokio paths; document intentional synchronous subprocesses with #[expect(clippy::disallowed_methods, reason = \"...\")]\n = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#disallowed_methods\n = note: requested on the command line with `-D clippy::disallowed-methods`\n\nerror: could not compile `fabro-server` (lib) due to 1 previous error\n", + "command.output": "", "last_stage": "fix_lints", "graph.model_stylesheet": "\n * { model: claude-opus-4-6; }\n ", "thread.toolchain.current_node": "preflight_compile", + "thread.fix_lints.current_node": "preflight_lint", "command.stderr": "", "thread.preflight_lint.current_node": "fix_lints", - "current_node": "fix_lints", + "current_node": "preflight_lint", "last_response": "Both clippy lint warnings are now fixed:\n\n1. **`lib/crates/fabro-server/src/serve.rs:504`** — Added `#[expect(clippy::disallowed_methods, reason = \"Intentional synchronous spawn before entering the ", "internal.retry_count.fix_lints": 0, "internal.run_id": "01KPBMNP4KCCNXXXMYG1Y1Z4EP", @@ -28,11 +30,11 @@ "thread.preflight_compile.current_node": "preflight_lint", "graph.rankdir": "LR", "outcome": "success", - "internal.node_visit_count": 1, + "internal.node_visit_count": 2, "thread.start.current_node": "toolchain", "failure_class": "", "failure_signature": "", - "internal.thread_id": "preflight_lint", + "internal.thread_id": "fix_lints", "internal.retry_count.preflight_lint": 0, "internal.retry_count.start": 0 }, @@ -92,15 +94,12 @@ ] }, "preflight_lint": { - "status": "fail", + "status": "success", "context_updates": { - "command.output": "error: use of a disallowed method `std::process::Command::new`\n --> lib/crates/fabro-server/src/serve.rs:504:21\n |\n504 | let child = std::process::Command::new(\"bun\")\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^\n |\n = note: Prefer tokio::process::Command on Tokio paths; document intentional synchronous subprocesses with #[expect(clippy::disallowed_methods, reason = \"...\")]\n = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#disallowed_methods\n = note: requested on the command line with `-D clippy::disallowed-methods`\n\nerror: could not compile `fabro-server` (lib) due to 1 previous error\n", + "command.output": "", "command.stderr": "" }, - "failure": { - "message": "Script failed with exit code: 101\n\n## stdout\nerror: use of a disallowed method `std::process::Command::new`\n --> lib/crates/fabro-server/src/serve.rs:504:21\n |\n504 | let child = std::process::Command::new(\"bun\")\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^\n |\n = note: Prefer tokio::process::Command on Tokio paths; document intentional synchronous subprocesses with #[expect(clippy::disallowed_methods, reason = \"...\")]\n = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#disallowed_methods\n = note: requested on the command line with `-D clippy::disallowed-methods`\n\nerror: could not compile `fabro-server` (lib) due to 1 previous error\n", - "failure_class": "transient_infra" - }, + "notes": "Script completed: cargo clippy -q --workspace -- -D warnings 2>&1", "usage": null }, "start": { @@ -108,12 +107,12 @@ "usage": null } }, - "next_node_id": "preflight_lint", + "next_node_id": "implement", "node_visits": { "toolchain": 1, "preflight_compile": 1, "start": 1, - "preflight_lint": 1, + "preflight_lint": 2, "fix_lints": 1 } } \ No newline at end of file diff --git a/nodes/fix_lints/diff.patch b/nodes/fix_lints/diff.patch new file mode 100644 index 000000000..43c0db40b --- /dev/null +++ b/nodes/fix_lints/diff.patch @@ -0,0 +1,47606 @@ +diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs +index f5a707c4..80b95759 100644 +--- a/lib/crates/fabro-agent/src/compaction.rs ++++ b/lib/crates/fabro-agent/src/compaction.rs +@@ -28,18 +28,21 @@ pub fn check_context_usage( + let threshold = context_window * threshold_percent / 100; + + if estimated_tokens > threshold { +- emitter.emit(session_id.to_owned(), AgentEvent::Warning { +- kind: "context_window".into(), +- message: format!( +- "Context window usage: {}%", +- estimated_tokens * 100 / context_window +- ), +- details: serde_json::json!({ +- "estimated_tokens": estimated_tokens, +- "context_window_size": context_window, +- "usage_percent": estimated_tokens * 100 / context_window, +- }), +- }); ++ emitter.emit( ++ session_id.to_owned(), ++ AgentEvent::Warning { ++ kind: "context_window".into(), ++ message: format!( ++ "Context window usage: {}%", ++ estimated_tokens * 100 / context_window ++ ), ++ details: serde_json::json!({ ++ "estimated_tokens": estimated_tokens, ++ "context_window_size": context_window, ++ "usage_percent": estimated_tokens * 100 / context_window, ++ }), ++ }, ++ ); + true + } else { + false +@@ -63,10 +66,13 @@ pub async fn compact_context( + let context_window = provider_profile.context_window_size(); + let original_turn_count = history.turns().len(); + +- emitter.emit(session_id.to_owned(), AgentEvent::CompactionStarted { +- estimated_tokens, +- context_window_size: context_window, +- }); ++ emitter.emit( ++ session_id.to_owned(), ++ AgentEvent::CompactionStarted { ++ estimated_tokens, ++ context_window_size: context_window, ++ }, ++ ); + + // Determine turns to summarize + if original_turn_count <= preserve_count { +@@ -102,24 +108,24 @@ function names, error messages, and exact values. Omit pleasantries and conversa + ); + + let summary_request = Request { +- model: provider_profile.model().to_string(), +- messages: vec![ ++ model: provider_profile.model().to_string(), ++ messages: vec![ + Message::system(summarization_prompt), + Message::user(format!( + "Here is the conversation to summarize:\n\n{rendered}" + )), + ], +- provider: Some(provider_profile.provider().as_str().to_string()), +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: Some(4096), +- stop_sequences: None, ++ provider: Some(provider_profile.provider().as_str().to_string()), ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: Some(4096), ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + }; + +@@ -141,12 +147,15 @@ Build on their progress — do not repeat completed steps.\n\n{summary_text}" + + history.compact(preserve_count, summary_content); + +- emitter.emit(session_id.to_owned(), AgentEvent::CompactionCompleted { +- original_turn_count, +- preserved_turn_count: preserve_count, +- summary_token_estimate, +- tracked_file_count: file_tracker.file_count(), +- }); ++ emitter.emit( ++ session_id.to_owned(), ++ AgentEvent::CompactionCompleted { ++ original_turn_count, ++ preserved_turn_count: preserve_count, ++ summary_token_estimate, ++ tracked_file_count: file_tracker.file_count(), ++ }, ++ ); + + Ok(()) + } +@@ -259,27 +268,27 @@ mod tests { + fn render_turns_produces_labeled_text() { + let turns = vec![ + Turn::User { +- content: "Hello".into(), ++ content: "Hello".into(), + timestamp: SystemTime::now(), + }, + Turn::Assistant { +- content: "Let me check".into(), +- tool_calls: vec![ToolCall::new( ++ content: "Let me check".into(), ++ tool_calls: vec![ToolCall::new( + "c1", + "read_file", + serde_json::json!({"path": "foo.rs"}), + )], + provider_parts: vec![], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_1".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_1".into(), ++ timestamp: SystemTime::now(), + }, + Turn::ToolResults { +- results: vec![ToolResult { +- tool_call_id: "c1".into(), +- content: serde_json::json!("file contents here"), +- is_error: false, +- image_data: None, ++ results: vec![ToolResult { ++ tool_call_id: "c1".into(), ++ content: serde_json::json!("file contents here"), ++ is_error: false, ++ image_data: None, + image_media_type: None, + }], + timestamp: SystemTime::now(), +@@ -298,11 +307,11 @@ mod tests { + fn render_turns_truncates_long_tool_output() { + let long_output = "x".repeat(1000); + let turns = vec![Turn::ToolResults { +- results: vec![ToolResult { +- tool_call_id: "c1".into(), +- content: serde_json::json!(long_output), +- is_error: false, +- image_data: None, ++ results: vec![ToolResult { ++ tool_call_id: "c1".into(), ++ content: serde_json::json!(long_output), ++ is_error: false, ++ image_data: None, + image_media_type: None, + }], + timestamp: SystemTime::now(), +@@ -317,7 +326,7 @@ mod tests { + fn estimate_token_count_basic() { + let mut history = History::default(); + history.push(Turn::User { +- content: "Hello world".into(), // 11 chars ++ content: "Hello world".into(), // 11 chars + timestamp: SystemTime::now(), + }); + // system_prompt = "test" (4 chars) + 11 chars = 15 chars / 4 = 3 tokens +@@ -339,7 +348,7 @@ mod tests { + let mut history = History::default(); + // Push enough content to exceed a tiny context window + history.push(Turn::User { +- content: "x".repeat(1000), ++ content: "x".repeat(1000), + timestamp: SystemTime::now(), + }); + let emitter = Emitter::new(); +diff --git a/lib/crates/fabro-agent/src/config.rs b/lib/crates/fabro-agent/src/config.rs +index 6e2f626b..34469436 100644 +--- a/lib/crates/fabro-agent/src/config.rs ++++ b/lib/crates/fabro-agent/src/config.rs +@@ -216,9 +216,12 @@ mod tests { + let approval: ToolApprovalFn = Arc::new(|_name, _args| Err("denied".to_string())); + let adapter = ToolApprovalAdapter(approval); + let decision = adapter.pre_tool_use("shell", &serde_json::json!({})).await; +- assert_eq!(decision, ToolHookDecision::Block { +- reason: "denied".to_string(), +- }); ++ assert_eq!( ++ decision, ++ ToolHookDecision::Block { ++ reason: "denied".to_string(), ++ } ++ ); + } + + #[tokio::test] +diff --git a/lib/crates/fabro-agent/src/error.rs b/lib/crates/fabro-agent/src/error.rs +index be5bbf05..13ce6aec 100644 +--- a/lib/crates/fabro-agent/src/error.rs ++++ b/lib/crates/fabro-agent/src/error.rs +@@ -48,7 +48,7 @@ mod tests { + fn agent_error_from_sdk_error() { + let sdk_err = LlmError::Network { + message: "connection refused".into(), +- source: None, ++ source: None, + }; + let agent_err = Error::from(sdk_err); + assert!(matches!(agent_err, Error::Llm(_))); +@@ -91,7 +91,7 @@ mod tests { + fn serde_roundtrip_llm_network() { + let err = Error::Llm(LlmError::Network { + message: "connection refused".into(), +- source: None, ++ source: None, + }); + let json = serde_json::to_string(&err).unwrap(); + let deserialized: Error = serde_json::from_str(&json).unwrap(); +@@ -101,14 +101,14 @@ mod tests { + #[test] + fn serde_roundtrip_llm_provider() { + let err = Error::Llm(LlmError::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail { +- message: "too fast".into(), +- provider: "openai".into(), ++ message: "too fast".into(), ++ provider: "openai".into(), + status_code: Some(429), +- error_code: None, ++ error_code: None, + retry_after: Some(2.0), +- raw: None, ++ raw: None, + }), + }); + let json = serde_json::to_string(&err).unwrap(); +@@ -155,7 +155,7 @@ mod tests { + let errors: Vec = vec![ + Error::Llm(LlmError::Network { + message: "refused".into(), +- source: None, ++ source: None, + }), + Error::SessionClosed, + Error::InvalidState("reason".into()), +@@ -173,7 +173,7 @@ mod tests { + fn serde_tag_format_llm() { + let err = Error::Llm(LlmError::Network { + message: "refused".into(), +- source: None, ++ source: None, + }); + let json = serde_json::to_string(&err).unwrap(); + let v: serde_json::Value = serde_json::from_str(&json).unwrap(); +diff --git a/lib/crates/fabro-agent/src/event.rs b/lib/crates/fabro-agent/src/event.rs +index c2495f25..a7a0f14c 100644 +--- a/lib/crates/fabro-agent/src/event.rs ++++ b/lib/crates/fabro-agent/src/event.rs +@@ -54,16 +54,22 @@ mod tests { + let emitter = Emitter::new(); + let mut receiver = emitter.subscribe(); + +- emitter.emit("sess-1".into(), AgentEvent::SessionStarted { +- provider: Some("anthropic".into()), +- model: Some("claude-opus".into()), +- }); ++ emitter.emit( ++ "sess-1".into(), ++ AgentEvent::SessionStarted { ++ provider: Some("anthropic".into()), ++ model: Some("claude-opus".into()), ++ }, ++ ); + + let event = receiver.recv().await.unwrap(); +- assert!(matches!(event.event, AgentEvent::SessionStarted { +- provider: Some(_), +- model: Some(_), +- })); ++ assert!(matches!( ++ event.event, ++ AgentEvent::SessionStarted { ++ provider: Some(_), ++ model: Some(_), ++ } ++ )); + assert_eq!(event.session_id, "sess-1"); + assert_eq!(event.parent_session_id, None); + } +@@ -73,9 +79,12 @@ mod tests { + let emitter = Emitter::new(); + let mut receiver = emitter.subscribe(); + +- emitter.emit("sess-2".into(), AgentEvent::Error { +- error: Error::ToolExecution("something went wrong".into()), +- }); ++ emitter.emit( ++ "sess-2".into(), ++ AgentEvent::Error { ++ error: Error::ToolExecution("something went wrong".into()), ++ }, ++ ); + + let event = receiver.recv().await.unwrap(); + assert!( +@@ -105,9 +114,12 @@ mod tests { + #[test] + fn emit_without_subscribers_does_not_panic() { + let emitter = Emitter::new(); +- emitter.emit("sess-4".into(), AgentEvent::Error { +- error: Error::ToolExecution("test".into()), +- }); ++ emitter.emit( ++ "sess-4".into(), ++ AgentEvent::Error { ++ error: Error::ToolExecution("test".into()), ++ }, ++ ); + } + + #[test] +@@ -122,21 +134,24 @@ mod tests { + let mut receiver = emitter.subscribe(); + + emitter.forward(SessionEvent { +- event: AgentEvent::SessionStarted { ++ event: AgentEvent::SessionStarted { + provider: Some("anthropic".into()), +- model: Some("claude-opus".into()), ++ model: Some("claude-opus".into()), + }, +- timestamp: SystemTime::now(), +- session_id: "child".into(), ++ timestamp: SystemTime::now(), ++ session_id: "child".into(), + parent_session_id: Some("parent".into()), + }); + + let event = receiver.recv().await.unwrap(); + assert_eq!(event.session_id, "child"); + assert_eq!(event.parent_session_id.as_deref(), Some("parent")); +- assert!(matches!(event.event, AgentEvent::SessionStarted { +- provider: Some(_), +- model: Some(_), +- })); ++ assert!(matches!( ++ event.event, ++ AgentEvent::SessionStarted { ++ provider: Some(_), ++ model: Some(_), ++ } ++ )); + } + } +diff --git a/lib/crates/fabro-agent/src/file_tracker.rs b/lib/crates/fabro-agent/src/file_tracker.rs +index 10d6e274..ed8cc04f 100644 +--- a/lib/crates/fabro-agent/src/file_tracker.rs ++++ b/lib/crates/fabro-agent/src/file_tracker.rs +@@ -5,9 +5,9 @@ use fabro_llm::types::{ToolCall, ToolResult}; + + #[derive(Debug, Clone, Copy, Default)] + struct FileOps { +- read: bool, ++ read: bool, + written: bool, +- edited: bool, ++ edited: bool, + } + + #[derive(Debug, Default)] +diff --git a/lib/crates/fabro-agent/src/history.rs b/lib/crates/fabro-agent/src/history.rs +index 74593dbc..cf439e20 100644 +--- a/lib/crates/fabro-agent/src/history.rs ++++ b/lib/crates/fabro-agent/src/history.rs +@@ -26,7 +26,7 @@ impl History { + let extracted_user_messages = + extract_recent_user_messages(discarded, COMPACTION_USER_MESSAGE_TOKEN_BUDGET); + self.turns.push(Turn::System { +- content: summary, ++ content: summary, + timestamp: std::time::SystemTime::now(), + }); + self.turns.extend(extracted_user_messages); +@@ -72,9 +72,9 @@ impl History { + parts.push(ContentPart::ToolCall(tc.clone())); + } + Message { +- role: Role::Assistant, +- content: parts, +- name: None, ++ role: Role::Assistant, ++ content: parts, ++ name: None, + tool_call_id: None, + } + } +@@ -94,9 +94,9 @@ impl History { + } + Turn::System { content, .. } => Message::system(content), + Turn::Steering { content, .. } => Message { +- role: Role::User, +- content: vec![ContentPart::text(content)], +- name: None, ++ role: Role::User, ++ content: vec![ContentPart::text(content)], ++ name: None, + tool_call_id: None, + }, + }) +@@ -149,7 +149,7 @@ mod tests { + let mut history = History::default(); + for i in 0..8 { + history.push(Turn::User { +- content: format!("msg {i}"), ++ content: format!("msg {i}"), + timestamp: SystemTime::now(), + }); + } +@@ -163,7 +163,7 @@ mod tests { + let mut history = History::default(); + for i in 0..3 { + history.push(Turn::User { +- content: format!("msg {i}"), ++ content: format!("msg {i}"), + timestamp: SystemTime::now(), + }); + } +@@ -176,7 +176,7 @@ mod tests { + let mut history = History::default(); + for i in 0..8 { + history.push(Turn::User { +- content: format!("msg {i}"), ++ content: format!("msg {i}"), + timestamp: SystemTime::now(), + }); + } +@@ -199,7 +199,7 @@ mod tests { + let mut history = History::default(); + for i in 0..6 { + history.push(Turn::User { +- content: format!("msg {i}"), ++ content: format!("msg {i}"), + timestamp: SystemTime::now(), + }); + } +@@ -220,7 +220,7 @@ mod tests { + fn user_turn_maps_to_user_message() { + let mut history = History::default(); + history.push(Turn::User { +- content: "Hello".into(), ++ content: "Hello".into(), + timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); +@@ -233,12 +233,12 @@ mod tests { + fn assistant_turn_maps_to_assistant_message() { + let mut history = History::default(); + history.push(Turn::Assistant { +- content: "Hi there".into(), +- tool_calls: vec![], ++ content: "Hi there".into(), ++ tool_calls: vec![], + provider_parts: vec![], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_1".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_1".into(), ++ timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); + assert_eq!(messages.len(), 1); +@@ -251,12 +251,12 @@ mod tests { + let mut history = History::default(); + let tc = ToolCall::new("call_1", "read_file", serde_json::json!({"path": "foo.rs"})); + history.push(Turn::Assistant { +- content: "Let me read that".into(), +- tool_calls: vec![tc], ++ content: "Let me read that".into(), ++ tool_calls: vec![tc], + provider_parts: vec![], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_2".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_2".into(), ++ timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); + assert_eq!(messages[0].role, Role::Assistant); +@@ -272,17 +272,17 @@ mod tests { + fn assistant_turn_with_reasoning_in_provider_parts() { + let mut history = History::default(); + let thinking = ContentPart::Thinking(ThinkingData { +- text: "Let me think about this...".into(), ++ text: "Let me think about this...".into(), + signature: None, +- redacted: false, ++ redacted: false, + }); + history.push(Turn::Assistant { +- content: "The answer is 42".into(), +- tool_calls: vec![], ++ content: "The answer is 42".into(), ++ tool_calls: vec![], + provider_parts: vec![thinking], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_3".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_3".into(), ++ timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); + let thinking_parts: Vec<_> = messages[0] +@@ -297,17 +297,17 @@ mod tests { + fn thinking_with_signature_preserved_via_provider_parts() { + let mut history = History::default(); + let thinking = ContentPart::Thinking(ThinkingData { +- text: "Let me think...".into(), ++ text: "Let me think...".into(), + signature: Some("sig_abc123".into()), +- redacted: false, ++ redacted: false, + }); + history.push(Turn::Assistant { +- content: "The answer".into(), +- tool_calls: vec![], ++ content: "The answer".into(), ++ tool_calls: vec![], + provider_parts: vec![thinking], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_4".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_4".into(), ++ timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); + let thinking_parts: Vec<_> = messages[0] +@@ -333,12 +333,12 @@ mod tests { + }; + let tc = ToolCall::new("call_1", "search", serde_json::json!({})); + history.push(Turn::Assistant { +- content: String::new(), +- tool_calls: vec![tc], ++ content: String::new(), ++ tool_calls: vec![tc], + provider_parts: vec![reasoning_item], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_1".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_1".into(), ++ timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); + assert_eq!(messages.len(), 1); +@@ -354,7 +354,7 @@ mod tests { + let mut history = History::default(); + let result = ToolResult::success("call_1", serde_json::json!("file contents here")); + history.push(Turn::ToolResults { +- results: vec![result], ++ results: vec![result], + timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); +@@ -367,7 +367,7 @@ mod tests { + fn system_turn_maps_to_system_message() { + let mut history = History::default(); + history.push(Turn::System { +- content: "You are a coding assistant".into(), ++ content: "You are a coding assistant".into(), + timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); +@@ -380,7 +380,7 @@ mod tests { + fn steering_turn_maps_to_user_message() { + let mut history = History::default(); + history.push(Turn::Steering { +- content: "Focus on the main task".into(), ++ content: "Focus on the main task".into(), + timestamp: SystemTime::now(), + }); + let messages = history.convert_to_messages(); +@@ -394,17 +394,17 @@ mod tests { + let mut history = History::default(); + assert_eq!(history.turns().len(), 0); + history.push(Turn::User { +- content: "First".into(), ++ content: "First".into(), + timestamp: SystemTime::now(), + }); + assert_eq!(history.turns().len(), 1); + history.push(Turn::Assistant { +- content: "Second".into(), +- tool_calls: vec![], ++ content: "Second".into(), ++ tool_calls: vec![], + provider_parts: vec![], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_1".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_1".into(), ++ timestamp: SystemTime::now(), + }); + assert_eq!(history.turns().len(), 2); + } +@@ -413,31 +413,31 @@ mod tests { + fn round_trip_preserves_content() { + let mut history = History::default(); + history.push(Turn::User { +- content: "Hello".into(), ++ content: "Hello".into(), + timestamp: SystemTime::now(), + }); + history.push(Turn::Assistant { +- content: "Hi".into(), +- tool_calls: vec![ToolCall::new( ++ content: "Hi".into(), ++ tool_calls: vec![ToolCall::new( + "c1", + "shell", + serde_json::json!({"cmd": "ls"}), + )], + provider_parts: vec![ContentPart::Thinking(ThinkingData { +- text: "thinking...".into(), ++ text: "thinking...".into(), + signature: None, +- redacted: false, ++ redacted: false, + })], +- usage: Box::new(TokenCounts { ++ usage: Box::new(TokenCounts { + input_tokens: 10, + output_tokens: 5, + ..Default::default() + }), +- response_id: "resp_1".into(), +- timestamp: SystemTime::now(), ++ response_id: "resp_1".into(), ++ timestamp: SystemTime::now(), + }); + history.push(Turn::ToolResults { +- results: vec![ToolResult::success( ++ results: vec![ToolResult::success( + "c1", + serde_json::json!("file1.rs\nfile2.rs"), + )], +@@ -455,11 +455,11 @@ mod tests { + fn compact_strips_openai_reasoning_from_preserved_turns() { + let mut history = History::default(); + history.push(Turn::User { +- content: "old msg".into(), ++ content: "old msg".into(), + timestamp: SystemTime::now(), + }); + history.push(Turn::User { +- content: "recent msg".into(), ++ content: "recent msg".into(), + timestamp: SystemTime::now(), + }); + let reasoning = ContentPart::Other { +@@ -468,12 +468,12 @@ mod tests { + }; + let tc = ToolCall::new("call_1", "search", serde_json::json!({})); + history.push(Turn::Assistant { +- content: "response".into(), +- tool_calls: vec![tc], ++ content: "response".into(), ++ tool_calls: vec![tc], + provider_parts: vec![reasoning], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_1".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_1".into(), ++ timestamp: SystemTime::now(), + }); + + history.compact(2, "Summary".into()); +@@ -503,25 +503,25 @@ mod tests { + fn compact_preserves_anthropic_thinking_blocks() { + let mut history = History::default(); + history.push(Turn::User { +- content: "old msg".into(), ++ content: "old msg".into(), + timestamp: SystemTime::now(), + }); + history.push(Turn::User { +- content: "recent msg".into(), ++ content: "recent msg".into(), + timestamp: SystemTime::now(), + }); + let thinking = ContentPart::Thinking(ThinkingData { +- text: "deep thought".into(), ++ text: "deep thought".into(), + signature: Some("sig_xyz".into()), +- redacted: false, ++ redacted: false, + }); + history.push(Turn::Assistant { +- content: "answer".into(), +- tool_calls: vec![], ++ content: "answer".into(), ++ tool_calls: vec![], + provider_parts: vec![thinking], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp_1".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp_1".into(), ++ timestamp: SystemTime::now(), + }); + + history.compact(2, "Summary".into()); +@@ -545,21 +545,21 @@ mod tests { + fn compact_strips_reasoning_from_all_preserved_assistant_turns() { + let mut history = History::default(); + history.push(Turn::User { +- content: "old msg".into(), ++ content: "old msg".into(), + timestamp: SystemTime::now(), + }); + // Two assistant turns that will both be preserved + for i in 0..2 { + history.push(Turn::Assistant { +- content: format!("response {i}"), +- tool_calls: vec![], ++ content: format!("response {i}"), ++ tool_calls: vec![], + provider_parts: vec![ContentPart::Other { + kind: ContentPart::OPENAI_REASONING.into(), + data: serde_json::json!({"type": "reasoning", "id": format!("rs_{i}")}), + }], +- usage: Box::new(TokenCounts::default()), +- response_id: format!("resp_{i}"), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: format!("resp_{i}"), ++ timestamp: SystemTime::now(), + }); + } + +@@ -579,19 +579,19 @@ mod tests { + fn extract_recent_user_messages_collects_in_chronological_order() { + let turns = vec![ + Turn::User { +- content: "first".into(), ++ content: "first".into(), + timestamp: SystemTime::now(), + }, + Turn::Assistant { +- content: "reply".into(), +- tool_calls: vec![], ++ content: "reply".into(), ++ tool_calls: vec![], + provider_parts: vec![], +- usage: Box::new(TokenCounts::default()), +- response_id: "r1".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "r1".into(), ++ timestamp: SystemTime::now(), + }, + Turn::User { +- content: "second".into(), ++ content: "second".into(), + timestamp: SystemTime::now(), + }, + ]; +@@ -605,11 +605,11 @@ mod tests { + fn extract_recent_user_messages_respects_token_budget() { + let turns = vec![ + Turn::User { +- content: "a".repeat(100), ++ content: "a".repeat(100), + timestamp: SystemTime::now(), + }, + Turn::User { +- content: "b".repeat(100), ++ content: "b".repeat(100), + timestamp: SystemTime::now(), + }, + ]; +@@ -624,19 +624,19 @@ mod tests { + fn compact_extracts_only_user_turns_from_discarded() { + let mut history = History::default(); + history.push(Turn::User { +- content: "user msg".into(), ++ content: "user msg".into(), + timestamp: SystemTime::now(), + }); + history.push(Turn::Assistant { +- content: "assistant msg".into(), +- tool_calls: vec![], ++ content: "assistant msg".into(), ++ tool_calls: vec![], + provider_parts: vec![], +- usage: Box::new(TokenCounts::default()), +- response_id: "r1".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "r1".into(), ++ timestamp: SystemTime::now(), + }); + history.push(Turn::User { +- content: "preserved".into(), ++ content: "preserved".into(), + timestamp: SystemTime::now(), + }); + +diff --git a/lib/crates/fabro-agent/src/loop_detection.rs b/lib/crates/fabro-agent/src/loop_detection.rs +index 926bd2a3..2bb0d655 100644 +--- a/lib/crates/fabro-agent/src/loop_detection.rs ++++ b/lib/crates/fabro-agent/src/loop_detection.rs +@@ -103,12 +103,12 @@ mod tests { + + fn assistant_with_tool(name: &str, args: serde_json::Value) -> Turn { + Turn::Assistant { +- content: String::new(), +- tool_calls: vec![ToolCall::new("call_1", name, args)], ++ content: String::new(), ++ tool_calls: vec![ToolCall::new("call_1", name, args)], + provider_parts: vec![], +- usage: Box::new(TokenCounts::default()), +- response_id: "resp".into(), +- timestamp: SystemTime::now(), ++ usage: Box::new(TokenCounts::default()), ++ response_id: "resp".into(), ++ timestamp: SystemTime::now(), + } + } + +@@ -266,15 +266,15 @@ mod tests { + fn user_turns_are_ignored() { + let mut history = History::default(); + history.push(Turn::User { +- content: "hello".into(), ++ content: "hello".into(), + timestamp: SystemTime::now(), + }); + history.push(Turn::User { +- content: "hello".into(), ++ content: "hello".into(), + timestamp: SystemTime::now(), + }); + history.push(Turn::User { +- content: "hello".into(), ++ content: "hello".into(), + timestamp: SystemTime::now(), + }); + assert!(!detect_loop(&history, 10)); +diff --git a/lib/crates/fabro-agent/src/mcp_integration.rs b/lib/crates/fabro-agent/src/mcp_integration.rs +index 65d8a3fe..10daffc8 100644 +--- a/lib/crates/fabro-agent/src/mcp_integration.rs ++++ b/lib/crates/fabro-agent/src/mcp_integration.rs +@@ -18,11 +18,11 @@ pub fn make_mcp_tools(manager: &Arc) -> Vec, +- pub is_git_repo: bool, +- pub current_date: String, +- pub model: String, +- pub knowledge_cutoff: String, +- pub git_status_short: Option, ++ pub git_branch: Option, ++ pub is_git_repo: bool, ++ pub current_date: String, ++ pub model: String, ++ pub knowledge_cutoff: String, ++ pub git_status_short: Option, + pub git_recent_commits: Option, + } + +@@ -133,12 +133,12 @@ mod tests { + fn env_context_block_with_extra_context() { + let env = MockSandbox::linux(); + let ctx = EnvContext { +- git_branch: Some("main".into()), +- is_git_repo: true, +- current_date: "2026-02-20".into(), +- model: "claude-opus-4-6".into(), +- knowledge_cutoff: "May 2025".into(), +- git_status_short: None, ++ git_branch: Some("main".into()), ++ is_git_repo: true, ++ current_date: "2026-02-20".into(), ++ model: "claude-opus-4-6".into(), ++ knowledge_cutoff: "May 2025".into(), ++ git_status_short: None, + git_recent_commits: None, + }; + let block = build_env_context_block_with(&env, &ctx); +diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs +index f50eec2b..4995ebe0 100644 +--- a/lib/crates/fabro-agent/src/session.rs ++++ b/lib/crates/fabro-agent/src/session.rs +@@ -38,24 +38,24 @@ use crate::tool_execution::execute_tool_calls; + use crate::types::{AgentEvent, SessionEvent, SessionState, Turn}; + + pub struct Session { +- id: String, +- config: SessionOptions, +- history: History, +- event_emitter: Emitter, +- state: SessionState, +- llm_client: Client, ++ id: String, ++ config: SessionOptions, ++ history: History, ++ event_emitter: Emitter, ++ state: SessionState, ++ llm_client: Client, + provider_profile: Arc, +- sandbox: Arc, +- steering_queue: Arc>>, +- followup_queue: Arc>>, +- cancel_token: CancellationToken, ++ sandbox: Arc, ++ steering_queue: Arc>>, ++ followup_queue: Arc>>, ++ cancel_token: CancellationToken, + interrupt_reason: Arc>>, +- memory: Vec, +- env_context: EnvContext, +- skills: Vec, +- system_prompt: String, +- file_tracker: FileTracker, +- tool_env: Option>, ++ memory: Vec, ++ env_context: EnvContext, ++ skills: Vec, ++ system_prompt: String, ++ file_tracker: FileTracker, ++ tool_env: Option>, + subagent_manager: Option>>, + } + +@@ -103,11 +103,13 @@ impl Session { + /// Initialize session by discovering project docs and capturing environment + /// context. Call before `process_input`. + pub async fn initialize(&mut self) { +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::SessionStarted { ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::SessionStarted { + provider: Some(self.provider_profile.provider().to_string()), +- model: Some(self.provider_profile.model().to_string()), +- }); ++ model: Some(self.provider_profile.model().to_string()), ++ }, ++ ); + + let doc_root = self + .config +@@ -155,18 +157,22 @@ impl Session { + for (server_name, result) in &results { + match result { + Ok(tool_count) => { +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::McpServerReady { ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::McpServerReady { + server_name: server_name.clone(), +- tool_count: *tool_count, +- }); ++ tool_count: *tool_count, ++ }, ++ ); + } + Err(e) => { +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::McpServerFailed { ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::McpServerFailed { + server_name: server_name.clone(), +- error: e.to_string(), +- }); ++ error: e.to_string(), ++ }, ++ ); + } + } + } +@@ -216,10 +222,10 @@ impl Session { + "Sandbox MCP server started, connecting via HTTP" + ); + resolved.push(McpServerSettings { +- name: config.name.clone(), +- transport: McpTransport::Http { url, headers }, ++ name: config.name.clone(), ++ transport: McpTransport::Http { url, headers }, + startup_timeout_secs: config.startup_timeout_secs, +- tool_timeout_secs: config.tool_timeout_secs, ++ tool_timeout_secs: config.tool_timeout_secs, + }); + } + Err(e) => { +@@ -228,11 +234,13 @@ impl Session { + error = %e, + "Failed to start sandbox MCP server" + ); +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::McpServerFailed { ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::McpServerFailed { + server_name: config.name.clone(), +- error: e, +- }); ++ error: e, ++ }, ++ ); + } + } + } +@@ -415,9 +423,12 @@ impl Session { + } + + fn emit_llm_error(&mut self, err: LlmError) -> Error { +- self.event_emitter.emit(self.id.clone(), AgentEvent::Error { +- error: Error::Llm(err.clone()), +- }); ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::Error { ++ error: Error::Llm(err.clone()), ++ }, ++ ); + if is_auth_error(&err) { + self.transition(SessionState::Closed); + } +@@ -622,29 +633,33 @@ impl Session { + // Expand skill references in input + let expanded = if self.skills.is_empty() { + ExpandedInput { +- text: input.to_string(), ++ text: input.to_string(), + skill_name: None, + } + } else { + expand_skill(&self.skills, input).map_err(Error::InvalidState)? + }; + if let Some(ref name) = expanded.skill_name { +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::SkillExpanded { ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::SkillExpanded { + skill_name: name.clone(), +- }); ++ }, ++ ); + } + let expanded_input = expanded.text; + + // Append user turn and emit event + self.history.push(Turn::User { +- content: expanded_input.clone(), ++ content: expanded_input.clone(), + timestamp: SystemTime::now(), + }); +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::UserInput { ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::UserInput { + text: expanded_input.clone(), +- }); ++ }, ++ ); + + // Drain steering queue before first LLM call + self.drain_steering(); +@@ -656,19 +671,23 @@ impl Session { + if self.config.max_tool_rounds_per_input > 0 + && round_count >= self.config.max_tool_rounds_per_input + { +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::TurnLimitReached { ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::TurnLimitReached { + max_turns: self.config.max_tool_rounds_per_input, +- }); ++ }, ++ ); + break; + } + + // Check max_turns + if self.config.max_turns > 0 && self.history.turns().len() >= self.config.max_turns { +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::TurnLimitReached { ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::TurnLimitReached { + max_turns: self.config.max_turns, +- }); ++ }, ++ ); + break; + } + +@@ -696,13 +715,16 @@ impl Session { + let retry_policy = RetryPolicy { + max_retries: 3, + on_retry: Some(std::sync::Arc::new(move |err, attempt, delay| { +- retry_emitter.emit(retry_session_id.clone(), AgentEvent::LlmRetry { +- provider: retry_provider.clone(), +- model: retry_model.clone(), +- attempt: attempt as usize, +- delay_secs: delay.as_secs_f64(), +- error: err.clone(), +- }); ++ retry_emitter.emit( ++ retry_session_id.clone(), ++ AgentEvent::LlmRetry { ++ provider: retry_provider.clone(), ++ model: retry_model.clone(), ++ attempt: attempt as usize, ++ delay_secs: delay.as_secs_f64(), ++ error: err.clone(), ++ }, ++ ); + })), + ..Default::default() + }; +@@ -782,7 +804,7 @@ impl Session { + self.event_emitter.emit( + self.id.clone(), + AgentEvent::AssistantOutputReplace { +- text: String::new(), ++ text: String::new(), + reasoning: None, + }, + ); +@@ -796,7 +818,7 @@ impl Session { + let Some(response) = response else { + return Err(self.emit_llm_error(LlmError::Stream { + message: "Stream ended without a Finish event (after retries)".into(), +- source: None, ++ source: None, + })); + }; + +@@ -822,13 +844,15 @@ impl Session { + }); + + // Emit AssistantMessage with enriched data from the response +- self.event_emitter +- .emit(self.id.clone(), AgentEvent::AssistantMessage { +- text: text.clone(), +- model: response.model.clone(), +- usage: response.usage.clone(), ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::AssistantMessage { ++ text: text.clone(), ++ model: response.model.clone(), ++ usage: response.usage.clone(), + tool_call_count: tool_calls.len(), +- }); ++ }, ++ ); + + // Post-response compaction: trim context after appending assistant turn + self.compact_if_needed().await; +@@ -918,9 +942,12 @@ impl Session { + ) + .await + { +- self.event_emitter.emit(self.id.clone(), AgentEvent::Error { +- error: Error::InvalidState(format!("Context compaction failed: {e}")), +- }); ++ self.event_emitter.emit( ++ self.id.clone(), ++ AgentEvent::Error { ++ error: Error::InvalidState(format!("Context compaction failed: {e}")), ++ }, ++ ); + } + } + } +@@ -935,7 +962,7 @@ impl Session { + for msg in messages { + let text = msg.clone(); + self.history.push(Turn::Steering { +- content: msg, ++ content: msg, + timestamp: SystemTime::now(), + }); + self.event_emitter +@@ -1013,7 +1040,7 @@ mod tests { + } + + struct ScriptedStreamProvider { +- calls: Vec, ++ calls: Vec, + call_index: AtomicUsize, + } + +@@ -1062,7 +1089,7 @@ mod tests { + async fn complete(&self, _request: &Request) -> Result { + Err(LlmError::Configuration { + message: "ScriptedStreamProvider does not implement complete()".into(), +- source: None, ++ source: None, + }) + } + +@@ -1444,11 +1471,11 @@ mod tests { + // Tool that cancels the token when executed + let abort_tool = RegisteredTool { + definition: ToolDefinition { +- name: "set_abort".into(), ++ name: "set_abort".into(), + description: "Sets interrupt flag".into(), +- parameters: serde_json::json!({"type": "object"}), ++ parameters: serde_json::json!({"type": "object"}), + }, +- executor: Arc::new(move |_args, _ctx| { ++ executor: Arc::new(move |_args, _ctx| { + let token = cancel_token_for_tool.clone(); + Box::pin(async move { + token.cancel(); +@@ -1497,7 +1524,7 @@ mod tests { + async fn auth_error_closes_session() { + let error_provider = Arc::new(MockErrorProvider { + error: LlmError::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail::new("invalid api key", "mock")), + }, + }); +@@ -1698,9 +1725,9 @@ mod tests { + let mut registry = ToolRegistry::new(); + registry.register(RegisteredTool { + definition: ToolDefinition { +- name: "strict_tool".into(), ++ name: "strict_tool".into(), + description: "Tool with required params".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "text": {"type": "string"} +@@ -1708,7 +1735,7 @@ mod tests { + "required": ["text"] + }), + }, +- executor: Arc::new(|_args, _ctx| { ++ executor: Arc::new(|_args, _ctx| { + Box::pin(async move { Ok("should not reach".to_string()) }) + }), + }); +@@ -1739,9 +1766,9 @@ mod tests { + let mut registry = ToolRegistry::new(); + registry.register(RegisteredTool { + definition: ToolDefinition { +- name: "strict_tool".into(), ++ name: "strict_tool".into(), + description: "Tool with required params".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "text": {"type": "string"} +@@ -1749,7 +1776,7 @@ mod tests { + "required": ["text"] + }), + }, +- executor: Arc::new(|_args, _ctx| { ++ executor: Arc::new(|_args, _ctx| { + Box::pin(async move { Ok("tool executed".to_string()) }) + }), + }); +@@ -2061,9 +2088,9 @@ mod tests { + async fn stream_mid_stream_error() { + let provider = Arc::new(MockMidStreamErrorProvider { + partial_text: "partial".into(), +- error: LlmError::Stream { ++ error: LlmError::Stream { + message: "connection reset".into(), +- source: None, ++ source: None, + }, + }); + let client = make_client(provider as Arc).await; +@@ -2146,19 +2173,22 @@ mod tests { + } + } + +- assert_eq!(observed, vec![ +- "start".to_string(), +- "delta:Hel".to_string(), +- "replace::None".to_string(), +- "delta:Hello".to_string(), +- "message:Hello".to_string(), +- ]); ++ assert_eq!( ++ observed, ++ vec![ ++ "start".to_string(), ++ "delta:Hel".to_string(), ++ "replace::None".to_string(), ++ "delta:Hello".to_string(), ++ "message:Hello".to_string(), ++ ] ++ ); + } + + #[tokio::test] + async fn retry_open_auth_error_emits_error_and_closes_session() { + let auth_error = LlmError::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail { + status_code: Some(401), + ..ProviderErrorDetail::new("bad key", "mock") +@@ -2207,12 +2237,15 @@ mod tests { + } + } + +- assert_eq!(observed, vec![ +- "start".to_string(), +- "delta:Hel".to_string(), +- "replace::None".to_string(), +- "error".to_string(), +- ]); ++ assert_eq!( ++ observed, ++ vec![ ++ "start".to_string(), ++ "delta:Hel".to_string(), ++ "replace::None".to_string(), ++ "error".to_string(), ++ ] ++ ); + assert!(found_auth_error_event, "expected auth error event"); + } + +@@ -2305,7 +2338,7 @@ mod tests { + // provider that errors on complete() but succeeds on stream(). + + struct StreamOnlyProvider { +- responses: Vec, ++ responses: Vec, + call_index: AtomicUsize, + } + +@@ -2318,7 +2351,7 @@ mod tests { + async fn complete(&self, _request: &Request) -> Result { + Err(LlmError::Stream { + message: "summarization failed".into(), +- source: None, ++ source: None, + }) + } + +@@ -2399,8 +2432,8 @@ mod tests { + // Provider that captures complete() requests (compaction) while returning + // canned responses for stream() calls. + struct CompactionCapturingProvider { +- stream_responses: Vec, +- stream_index: AtomicUsize, ++ stream_responses: Vec, ++ stream_index: AtomicUsize, + captured_complete: Mutex>, + } + +@@ -2429,11 +2462,11 @@ mod tests { + // read_file tool that always succeeds + let read_tool = RegisteredTool { + definition: ToolDefinition { +- name: "read_file".into(), ++ name: "read_file".into(), + description: "Read a file".into(), +- parameters: serde_json::json!({"type": "object", "properties": {"file_path": {"type": "string"}}}), ++ parameters: serde_json::json!({"type": "object", "properties": {"file_path": {"type": "string"}}}), + }, +- executor: Arc::new(|_args, _ctx| { ++ executor: Arc::new(|_args, _ctx| { + Box::pin(async move { Ok("file contents".to_string()) }) + }), + }; +@@ -2542,13 +2575,13 @@ mod tests { + ); + let config = SessionOptions { + mcp_servers: vec![McpServerSettings { +- name: "test-echo".into(), +- transport: McpTransport::Stdio { ++ name: "test-echo".into(), ++ transport: McpTransport::Stdio { + command: vec!["python3".into(), test_server], +- env: HashMap::new(), ++ env: HashMap::new(), + }, + startup_timeout_secs: 10, +- tool_timeout_secs: 30, ++ tool_timeout_secs: 30, + }], + enable_loop_detection: false, + ..Default::default() +@@ -2654,11 +2687,11 @@ mod tests { + // Register a tool that loops until the cancel token fires + let slow_tool = RegisteredTool { + definition: ToolDefinition { +- name: "slow_tool".into(), ++ name: "slow_tool".into(), + description: "Waits until cancelled".into(), +- parameters: serde_json::json!({"type": "object"}), ++ parameters: serde_json::json!({"type": "object"}), + }, +- executor: Arc::new(|_args, ctx| { ++ executor: Arc::new(|_args, ctx| { + Box::pin(async move { + ctx.cancel.cancelled().await; + Ok("cancelled".to_string()) +diff --git a/lib/crates/fabro-agent/src/skills.rs b/lib/crates/fabro-agent/src/skills.rs +index a4d00226..6c48f38d 100644 +--- a/lib/crates/fabro-agent/src/skills.rs ++++ b/lib/crates/fabro-agent/src/skills.rs +@@ -8,9 +8,9 @@ use crate::tools::required_str; + + #[derive(Debug, Clone)] + pub struct Skill { +- pub name: String, ++ pub name: String, + pub description: String, +- pub template: String, ++ pub template: String, + } + + pub fn parse_skill(content: &str) -> Result { +@@ -51,11 +51,11 @@ pub fn parse_skill(content: &str) -> Result { + /// A detected skill reference in user input: the name and byte range of the + /// `/name` token. + struct SkillMatch { +- name: String, ++ name: String, + /// Byte offset of the `/` character + start: usize, + /// Byte offset just past the skill name +- end: usize, ++ end: usize, + } + + fn is_skill_name_char(c: char) -> bool { +@@ -97,9 +97,9 @@ fn find_skill_references(input: &str) -> Vec { + let followed_by_boundary = j >= len || bytes[j].is_ascii_whitespace(); + if followed_by_boundary { + results.push(SkillMatch { +- name: input[name_start..j].to_string(), ++ name: input[name_start..j].to_string(), + start: i, +- end: j, ++ end: j, + }); + } + +@@ -114,7 +114,7 @@ fn find_skill_references(input: &str) -> Vec { + + #[derive(Debug)] + pub struct ExpandedInput { +- pub text: String, ++ pub text: String, + pub skill_name: Option, + } + +@@ -123,7 +123,7 @@ pub fn expand_skill(skills: &[Skill], input: &str) -> Result Result>) -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "use_skill".into(), ++ name: "use_skill".into(), + description: "Load a skill's instructions by name. Call this when the user's \ + request matches an available skill." + .into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "skill_name": { +@@ -174,7 +174,7 @@ pub fn make_use_skill_tool(skills: Arc>) -> RegisteredTool { + "required": ["skill_name"] + }), + }, +- executor: Arc::new(move |args, _ctx| { ++ executor: Arc::new(move |args, _ctx| { + let skills = skills.clone(); + Box::pin(async move { + let name = required_str(&args, "skill_name")?; +@@ -343,14 +343,14 @@ name: trimmed + fn test_skills() -> Vec { + vec![ + Skill { +- name: "commit".into(), ++ name: "commit".into(), + description: "Create a commit".into(), +- template: "Review changes and commit.\n\n{{user_input}}".into(), ++ template: "Review changes and commit.\n\n{{user_input}}".into(), + }, + Skill { +- name: "test".into(), ++ name: "test".into(), + description: "Run tests".into(), +- template: "Run the test suite.".into(), ++ template: "Run the test suite.".into(), + }, + ] + } +@@ -524,11 +524,14 @@ name: trimmed + #[test] + fn default_dirs_with_git_root() { + let dirs = default_skill_dirs(Some("/home/user/.fabro/skills"), Some("/repo")); +- assert_eq!(dirs, vec![ +- "/home/user/.fabro/skills", +- "/repo/.fabro/skills", +- "/repo/skills", +- ]); ++ assert_eq!( ++ dirs, ++ vec![ ++ "/home/user/.fabro/skills", ++ "/repo/.fabro/skills", ++ "/repo/skills", ++ ] ++ ); + } + + #[test] +diff --git a/lib/crates/fabro-agent/src/subagent.rs b/lib/crates/fabro-agent/src/subagent.rs +index a27cde76..0031422c 100644 +--- a/lib/crates/fabro-agent/src/subagent.rs ++++ b/lib/crates/fabro-agent/src/subagent.rs +@@ -24,8 +24,8 @@ pub type SubAgentEventCallback = Arc>>, ++ task: Option>>, + followup_queue: Arc>>, +- cancel_token: CancellationToken, +- depth: usize, +- status: SubAgentStatus, ++ cancel_token: CancellationToken, ++ depth: usize, ++ status: SubAgentStatus, + } + + pub struct SubAgentManager { +- agents: HashMap, +- max_depth: usize, ++ agents: HashMap, ++ max_depth: usize, + event_callback: Option, + } + +@@ -119,24 +119,27 @@ impl SubAgentManager { + _ => None, + }); + Ok(SubAgentResult { +- output: last_text.unwrap_or_default(), +- success: true, ++ output: last_text.unwrap_or_default(), ++ success: true, + turns_used: turns.len(), + }) + }); + +- self.agents.insert(agent_id.clone(), SubAgent { +- task: Some(task), +- followup_queue, +- cancel_token, +- depth: depth + 1, +- status: SubAgentStatus::Running, +- }); ++ self.agents.insert( ++ agent_id.clone(), ++ SubAgent { ++ task: Some(task), ++ followup_queue, ++ cancel_token, ++ depth: depth + 1, ++ status: SubAgentStatus::Running, ++ }, ++ ); + + self.emit_event(AgentEvent::SubAgentSpawned { + agent_id: agent_id.clone(), +- depth: depth + 1, +- task: task_prompt, ++ depth: depth + 1, ++ task: task_prompt, + }); + + Ok(agent_id) +@@ -303,9 +306,9 @@ pub fn make_spawn_agent_tool( + ) -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "spawn_agent".into(), ++ name: "spawn_agent".into(), + description: "Spawn a subagent to work on a delegated task".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "task": { +@@ -328,7 +331,7 @@ pub fn make_spawn_agent_tool( + "required": ["task"] + }), + }, +- executor: Arc::new(move |args, _ctx| { ++ executor: Arc::new(move |args, _ctx| { + let manager = manager.clone(); + let session_factory = session_factory.clone(); + Box::pin(async move { +@@ -356,9 +359,9 @@ pub fn make_spawn_agent_tool( + pub fn make_send_input_tool(manager: Arc>) -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "send_input".into(), ++ name: "send_input".into(), + description: "Send a follow-up message to a running subagent".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "agent_id": { +@@ -373,7 +376,7 @@ pub fn make_send_input_tool(manager: Arc>) -> Regist + "required": ["agent_id", "message"] + }), + }, +- executor: Arc::new(move |args, _ctx| { ++ executor: Arc::new(move |args, _ctx| { + let manager = manager.clone(); + Box::pin(async move { + let agent_id = required_str(&args, "agent_id")?; +@@ -391,9 +394,9 @@ pub fn make_send_input_tool(manager: Arc>) -> Regist + pub fn make_wait_tool(manager: Arc>) -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "wait".into(), ++ name: "wait".into(), + description: "Wait for a subagent to complete and return its result".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "agent_id": { +@@ -404,7 +407,7 @@ pub fn make_wait_tool(manager: Arc>) -> RegisteredTo + "required": ["agent_id"] + }), + }, +- executor: Arc::new(move |args, _ctx| { ++ executor: Arc::new(move |args, _ctx| { + let manager = manager.clone(); + Box::pin(async move { + let agent_id = required_str(&args, "agent_id")?; +@@ -423,9 +426,9 @@ pub fn make_wait_tool(manager: Arc>) -> RegisteredTo + pub fn make_close_agent_tool(manager: Arc>) -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "close_agent".into(), ++ name: "close_agent".into(), + description: "Close a running subagent".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "agent_id": { +@@ -436,7 +439,7 @@ pub fn make_close_agent_tool(manager: Arc>) -> Regis + "required": ["agent_id"] + }), + }, +- executor: Arc::new(move |args, _ctx| { ++ executor: Arc::new(move |args, _ctx| { + let manager = manager.clone(); + Box::pin(async move { + let agent_id = required_str(&args, "agent_id")?; +@@ -715,21 +718,21 @@ mod tests { + let mut rx = parent.subscribe(); + + callback(SubAgentCallbackEvent::Forwarded(SessionEvent { +- event: AgentEvent::SessionStarted { ++ event: AgentEvent::SessionStarted { + provider: Some("anthropic".into()), +- model: Some("claude-opus".into()), ++ model: Some("claude-opus".into()), + }, +- timestamp: std::time::SystemTime::now(), +- session_id: "child".into(), ++ timestamp: std::time::SystemTime::now(), ++ session_id: "child".into(), + parent_session_id: None, + })); + callback(SubAgentCallbackEvent::Forwarded(SessionEvent { +- event: AgentEvent::SessionStarted { ++ event: AgentEvent::SessionStarted { + provider: Some("anthropic".into()), +- model: Some("claude-opus".into()), ++ model: Some("claude-opus".into()), + }, +- timestamp: std::time::SystemTime::now(), +- session_id: "grandchild".into(), ++ timestamp: std::time::SystemTime::now(), ++ session_id: "grandchild".into(), + parent_session_id: Some("child".into()), + })); + +@@ -747,7 +750,7 @@ mod tests { + let manager = SubAgentManager::new(3); + manager.emit_event(AgentEvent::SubAgentClosed { + agent_id: "x".into(), +- depth: 0, ++ depth: 0, + }); + } + +diff --git a/lib/crates/fabro-agent/src/test_support.rs b/lib/crates/fabro-agent/src/test_support.rs +index d7284bc1..13f663f1 100644 +--- a/lib/crates/fabro-agent/src/test_support.rs ++++ b/lib/crates/fabro-agent/src/test_support.rs +@@ -24,14 +24,14 @@ use crate::tool_registry::{RegisteredTool, ToolRegistry}; + // --- TestProfile --- + + pub struct TestProfile { +- pub registry: ToolRegistry, ++ pub registry: ToolRegistry, + pub context_window: usize, + } + + impl TestProfile { + pub fn new() -> Self { + Self { +- registry: ToolRegistry::new(), ++ registry: ToolRegistry::new(), + context_window: 200_000, + } + } +@@ -98,7 +98,7 @@ impl AgentProfile for TestProfile { + // --- MockLlmProvider --- + + pub struct MockLlmProvider { +- pub responses: Vec, ++ pub responses: Vec, + pub call_index: AtomicUsize, + } + +@@ -170,19 +170,19 @@ pub fn response_to_stream(response: Response) -> StreamEventStream { + + pub fn text_response(text: &str) -> Response { + Response { +- id: format!("resp_{text}"), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(text), ++ id: format!("resp_{text}"), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 5, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + } + } + +@@ -238,27 +238,27 @@ pub fn tool_call_response( + ) -> Response { + use fabro_llm::types::{ContentPart, Role, ToolCall}; + Response { +- id: format!("resp_{tool_call_id}"), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message { +- role: Role::Assistant, +- content: vec![ ++ id: format!("resp_{tool_call_id}"), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message { ++ role: Role::Assistant, ++ content: vec![ + ContentPart::text("Let me use a tool."), + ContentPart::ToolCall(ToolCall::new(tool_call_id, tool_name, args)), + ], +- name: None, ++ name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 5, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + } + } + +@@ -266,11 +266,11 @@ pub fn make_echo_tool() -> RegisteredTool { + use fabro_llm::types::ToolDefinition; + RegisteredTool { + definition: ToolDefinition { +- name: "echo".into(), ++ name: "echo".into(), + description: "Echoes the input".into(), +- parameters: serde_json::json!({"type": "object", "properties": {"text": {"type": "string"}}}), ++ parameters: serde_json::json!({"type": "object", "properties": {"text": {"type": "string"}}}), + }, +- executor: Arc::new(|args, _ctx| { ++ executor: Arc::new(|args, _ctx| { + Box::pin(async move { + let text = args + .get("text") +@@ -286,11 +286,11 @@ pub fn make_error_tool() -> RegisteredTool { + use fabro_llm::types::ToolDefinition; + RegisteredTool { + definition: ToolDefinition { +- name: "fail_tool".into(), ++ name: "fail_tool".into(), + description: "Always fails".into(), +- parameters: serde_json::json!({"type": "object"}), ++ parameters: serde_json::json!({"type": "object"}), + }, +- executor: Arc::new(|_args, _ctx| { ++ executor: Arc::new(|_args, _ctx| { + Box::pin(async move { Err("tool execution failed".to_string()) }) + }), + } +@@ -360,7 +360,7 @@ impl ProviderAdapter for CapturingLlmProvider { + /// A mock provider that yields some text deltas then an error mid-stream. + pub struct MockMidStreamErrorProvider { + pub partial_text: String, +- pub error: LlmError, ++ pub error: LlmError, + } + + #[async_trait] +@@ -393,23 +393,23 @@ pub fn multi_tool_call_response(calls: Vec<(&str, &str, serde_json::Value)>) -> + ))); + } + Response { +- id: "resp_multi".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message { ++ id: "resp_multi".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message { + role: Role::Assistant, + content, + name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 5, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + } + } +diff --git a/lib/crates/fabro-agent/src/tool_execution.rs b/lib/crates/fabro-agent/src/tool_execution.rs +index 31bcfbf1..11ce189f 100644 +--- a/lib/crates/fabro-agent/src/tool_execution.rs ++++ b/lib/crates/fabro-agent/src/tool_execution.rs +@@ -180,11 +180,14 @@ async fn execute_and_emit_one_tool_with_lookup( + session_id: &str, + tool_env: Option<&HashMap>, + ) -> ToolResult { +- emitter.emit(session_id.to_owned(), AgentEvent::ToolCallStarted { +- tool_name: tc.name.clone(), +- tool_call_id: tc.id.clone(), +- arguments: tc.arguments.clone(), +- }); ++ emitter.emit( ++ session_id.to_owned(), ++ AgentEvent::ToolCallStarted { ++ tool_name: tc.name.clone(), ++ tool_call_id: tc.id.clone(), ++ arguments: tc.arguments.clone(), ++ }, ++ ); + + // Pre-tool-use hook + if let Some(hooks) = tool_hooks { +@@ -197,15 +200,21 @@ async fn execute_and_emit_one_tool_with_lookup( + if let ToolHookDecision::Block { reason } = decision { + let result = ToolResult::error(&tc.id, &reason); + +- emitter.emit(session_id.to_owned(), AgentEvent::ToolCallOutputDelta { +- delta: result.content.to_string(), +- }); +- emitter.emit(session_id.to_owned(), AgentEvent::ToolCallCompleted { +- tool_name: tc.name.clone(), +- tool_call_id: tc.id.clone(), +- output: result.content.clone(), +- is_error: true, +- }); ++ emitter.emit( ++ session_id.to_owned(), ++ AgentEvent::ToolCallOutputDelta { ++ delta: result.content.to_string(), ++ }, ++ ); ++ emitter.emit( ++ session_id.to_owned(), ++ AgentEvent::ToolCallCompleted { ++ tool_name: tc.name.clone(), ++ tool_call_id: tc.id.clone(), ++ output: result.content.clone(), ++ is_error: true, ++ }, ++ ); + + return truncate_tool_result(&result, &tc.name, config); + } +@@ -213,16 +222,22 @@ async fn execute_and_emit_one_tool_with_lookup( + + let result = execute_one_tool(tc, registered_tool, env, cancel_token, tool_env).await; + +- emitter.emit(session_id.to_owned(), AgentEvent::ToolCallOutputDelta { +- delta: result.content.to_string(), +- }); +- +- emitter.emit(session_id.to_owned(), AgentEvent::ToolCallCompleted { +- tool_name: tc.name.clone(), +- tool_call_id: tc.id.clone(), +- output: result.content.clone(), +- is_error: result.is_error, +- }); ++ emitter.emit( ++ session_id.to_owned(), ++ AgentEvent::ToolCallOutputDelta { ++ delta: result.content.to_string(), ++ }, ++ ); ++ ++ emitter.emit( ++ session_id.to_owned(), ++ AgentEvent::ToolCallCompleted { ++ tool_name: tc.name.clone(), ++ tool_call_id: tc.id.clone(), ++ output: result.content.clone(), ++ is_error: result.is_error, ++ }, ++ ); + + // Post-tool-use hooks + if let Some(hooks) = tool_hooks { +@@ -293,10 +308,10 @@ fn truncate_tool_result( + }; + + ToolResult { +- tool_call_id: result.tool_call_id.clone(), +- content: truncated_content, +- is_error: result.is_error, +- image_data: result.image_data.clone(), ++ tool_call_id: result.tool_call_id.clone(), ++ content: truncated_content, ++ is_error: result.is_error, ++ image_data: result.image_data.clone(), + image_media_type: result.image_media_type.clone(), + } + } +@@ -350,9 +365,9 @@ mod tests { + fn make_echo_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "echo".to_string(), ++ name: "echo".to_string(), + description: "Echo input".to_string(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "text": {"type": "string"} +@@ -360,7 +375,7 @@ mod tests { + "required": ["text"] + }), + }, +- executor: Arc::new(|args: serde_json::Value, _ctx: ToolContext| { ++ executor: Arc::new(|args: serde_json::Value, _ctx: ToolContext| { + Box::pin(async move { + let text = args["text"].as_str().unwrap_or("").to_string(); + Ok(format!("echo: {text}")) +@@ -372,11 +387,11 @@ mod tests { + fn make_fail_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "fail_tool".to_string(), ++ name: "fail_tool".to_string(), + description: "Always fails".to_string(), +- parameters: serde_json::json!({}), ++ parameters: serde_json::json!({}), + }, +- executor: Arc::new(|_args: serde_json::Value, _ctx: ToolContext| { ++ executor: Arc::new(|_args: serde_json::Value, _ctx: ToolContext| { + Box::pin(async move { Err("tool failed".to_string()) }) + }), + } +@@ -384,26 +399,26 @@ mod tests { + + fn make_tool_call(name: &str, id: &str, args: serde_json::Value) -> ToolCall { + ToolCall { +- id: id.to_string(), +- name: name.to_string(), +- tool_type: "function".to_string(), +- arguments: args, +- raw_arguments: None, ++ id: id.to_string(), ++ name: name.to_string(), ++ tool_type: "function".to_string(), ++ arguments: args, ++ raw_arguments: None, + provider_metadata: None, + } + } + + struct MockHookCallback { +- pre_decision: ToolHookDecision, +- post_calls: Arc>>, ++ pre_decision: ToolHookDecision, ++ post_calls: Arc>>, + post_failure_calls: Arc>>, + } + + impl MockHookCallback { + fn new(decision: ToolHookDecision) -> Self { + Self { +- pre_decision: decision, +- post_calls: Arc::new(Mutex::new(Vec::new())), ++ pre_decision: decision, ++ post_calls: Arc::new(Mutex::new(Vec::new())), + post_failure_calls: Arc::new(Mutex::new(Vec::new())), + } + } +diff --git a/lib/crates/fabro-agent/src/tool_registry.rs b/lib/crates/fabro-agent/src/tool_registry.rs +index cc098424..99780894 100644 +--- a/lib/crates/fabro-agent/src/tool_registry.rs ++++ b/lib/crates/fabro-agent/src/tool_registry.rs +@@ -9,8 +9,8 @@ use tokio_util::sync::CancellationToken; + use crate::sandbox::Sandbox; + + pub struct ToolContext { +- pub env: Arc, +- pub cancel: CancellationToken, ++ pub env: Arc, ++ pub cancel: CancellationToken, + pub tool_env: Option>, + } + +@@ -26,7 +26,7 @@ pub type ToolExecutor = Arc< + #[derive(Clone)] + pub struct RegisteredTool { + pub definition: ToolDefinition, +- pub executor: ToolExecutor, ++ pub executor: ToolExecutor, + } + + pub struct ToolRegistry { +@@ -80,11 +80,11 @@ mod tests { + fn make_tool(name: &str) -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: name.into(), ++ name: name.into(), + description: format!("Tool {name}"), +- parameters: serde_json::json!({"type": "object"}), ++ parameters: serde_json::json!({"type": "object"}), + }, +- executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("ok".into()) })), ++ executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("ok".into()) })), + } + } + +@@ -124,19 +124,19 @@ mod tests { + let mut registry = ToolRegistry::new(); + registry.register(RegisteredTool { + definition: ToolDefinition { +- name: "tool_a".into(), ++ name: "tool_a".into(), + description: "version 1".into(), +- parameters: serde_json::json!({}), ++ parameters: serde_json::json!({}), + }, +- executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("v1".into()) })), ++ executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("v1".into()) })), + }); + registry.register(RegisteredTool { + definition: ToolDefinition { +- name: "tool_a".into(), ++ name: "tool_a".into(), + description: "version 2".into(), +- parameters: serde_json::json!({}), ++ parameters: serde_json::json!({}), + }, +- executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("v2".into()) })), ++ executor: Arc::new(|_args, _ctx| Box::pin(async { Ok("v2".into()) })), + }); + + let tool = registry.get("tool_a").unwrap(); +diff --git a/lib/crates/fabro-agent/src/tools.rs b/lib/crates/fabro-agent/src/tools.rs +index c3b1a8bc..e6fa82be 100644 +--- a/lib/crates/fabro-agent/src/tools.rs ++++ b/lib/crates/fabro-agent/src/tools.rs +@@ -15,7 +15,7 @@ const MAX_WEB_FETCH_BYTES: usize = 100 * 1024; + /// Configuration for the optional LLM-based summarizer used by `web_fetch`. + #[derive(Clone)] + pub struct WebFetchSummarizer { +- pub client: Client, ++ pub client: Client, + pub model_id: ModelHandle, + } + +@@ -72,9 +72,9 @@ pub(crate) fn required_str<'a>(args: &'a serde_json::Value, key: &str) -> Result + pub fn make_read_file_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "read_file".into(), ++ name: "read_file".into(), + description: "Read the contents of a file".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "Absolute path to the file"}, +@@ -84,7 +84,7 @@ pub fn make_read_file_tool() -> RegisteredTool { + "required": ["file_path"] + }), + }, +- executor: Arc::new(|args, ctx| { ++ executor: Arc::new(|args, ctx| { + Box::pin(async move { + let file_path = required_str(&args, "file_path")?; + let offset = args.get("offset").and_then(serde_json::Value::as_u64); +@@ -108,9 +108,9 @@ pub fn make_read_file_tool() -> RegisteredTool { + pub fn make_write_file_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "write_file".into(), ++ name: "write_file".into(), + description: "Write content to a file".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "Absolute path to the file"}, +@@ -119,7 +119,7 @@ pub fn make_write_file_tool() -> RegisteredTool { + "required": ["file_path", "content"] + }), + }, +- executor: Arc::new(|args, ctx| { ++ executor: Arc::new(|args, ctx| { + Box::pin(async move { + let file_path = required_str(&args, "file_path")?; + let content = required_str(&args, "content")?; +@@ -135,9 +135,9 @@ pub fn make_write_file_tool() -> RegisteredTool { + pub fn make_edit_file_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "edit_file".into(), ++ name: "edit_file".into(), + description: "Edit a file by replacing a string".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "Absolute path to the file"}, +@@ -148,7 +148,7 @@ pub fn make_edit_file_tool() -> RegisteredTool { + "required": ["file_path", "old_string", "new_string"] + }), + }, +- executor: Arc::new(|args, ctx| { ++ executor: Arc::new(|args, ctx| { + Box::pin(async move { + let file_path = required_str(&args, "file_path")?; + let old_string = required_str(&args, "old_string")?; +@@ -201,9 +201,9 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { + let max_timeout = config.max_command_timeout_ms; + RegisteredTool { + definition: ToolDefinition { +- name: "shell".into(), ++ name: "shell".into(), + description: "Execute a shell command".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "command": {"type": "string", "description": "The shell command to execute"}, +@@ -213,7 +213,7 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { + "required": ["command"] + }), + }, +- executor: Arc::new(move |args, ctx| { ++ executor: Arc::new(move |args, ctx| { + Box::pin(async move { + let command = required_str(&args, "command")?; + let timeout_ms = args +@@ -259,9 +259,9 @@ pub fn make_shell_tool_with_config(config: &SessionOptions) -> RegisteredTool { + pub fn make_grep_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "grep".into(), ++ name: "grep".into(), + description: "Search file contents with a regex pattern".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "pattern": {"type": "string", "description": "Regex pattern to search for"}, +@@ -273,7 +273,7 @@ pub fn make_grep_tool() -> RegisteredTool { + "required": ["pattern"] + }), + }, +- executor: Arc::new(|args, ctx| { ++ executor: Arc::new(|args, ctx| { + Box::pin(async move { + let pattern = required_str(&args, "pattern")?; + let path = args +@@ -316,9 +316,9 @@ pub fn make_grep_tool() -> RegisteredTool { + pub fn make_glob_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "glob".into(), ++ name: "glob".into(), + description: "Find files matching a glob pattern".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "pattern": {"type": "string", "description": "Glob pattern to match files"}, +@@ -327,7 +327,7 @@ pub fn make_glob_tool() -> RegisteredTool { + "required": ["pattern"] + }), + }, +- executor: Arc::new(|args, ctx| { ++ executor: Arc::new(|args, ctx| { + Box::pin(async move { + let pattern = required_str(&args, "pattern")?; + let path = args.get("path").and_then(serde_json::Value::as_str); +@@ -343,9 +343,9 @@ pub fn make_glob_tool() -> RegisteredTool { + pub(crate) fn make_read_many_files_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "read_many_files".into(), ++ name: "read_many_files".into(), + description: "Read multiple files at once".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "paths": { +@@ -357,7 +357,7 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { + "required": ["paths"] + }), + }, +- executor: Arc::new(|args, ctx| { ++ executor: Arc::new(|args, ctx| { + Box::pin(async move { + let paths = args["paths"] + .as_array() +@@ -388,9 +388,9 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { + pub(crate) fn make_list_dir_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "list_dir".into(), ++ name: "list_dir".into(), + description: "List directory contents with depth control".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "path": {"type": "string", "description": "Directory path to list"}, +@@ -399,7 +399,7 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool { + "required": ["path"] + }), + }, +- executor: Arc::new(|args, ctx| { ++ executor: Arc::new(|args, ctx| { + Box::pin(async move { + let path = required_str(&args, "path")?; + let depth = args +@@ -471,9 +471,9 @@ fn make_web_search_tool_with_api_key(api_key: Option) -> RegisteredTool + + RegisteredTool { + definition: ToolDefinition { +- name: "web_search".into(), ++ name: "web_search".into(), + description: "Search the web using Brave Search".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"}, +@@ -482,7 +482,7 @@ fn make_web_search_tool_with_api_key(api_key: Option) -> RegisteredTool + "required": ["query"] + }), + }, +- executor: Arc::new(move |args, _ctx| { ++ executor: Arc::new(move |args, _ctx| { + let api_key = api_key.clone(); + Box::pin(async move { + let api_key = api_key.ok_or_else(|| { +@@ -642,11 +642,14 @@ mod tests { + apply_read_offset_limit: true, + ..Default::default() + }); +- let result = (tool.executor)(serde_json::json!({"file_path": "/test.txt"}), ToolContext { +- env, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let result = (tool.executor)( ++ serde_json::json!({"file_path": "/test.txt"}), ++ ToolContext { ++ env, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + assert_eq!(result.unwrap(), " 1 | hello\n 2 | world"); + } +@@ -684,8 +687,8 @@ mod tests { + let result = (tool.executor)( + serde_json::json!({"file_path": "/out.txt", "content": "hello"}), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: None, + }, + ) +@@ -714,8 +717,8 @@ mod tests { + "new_string": "goodbye" + }), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: None, + }, + ) +@@ -796,8 +799,8 @@ mod tests { + "replace_all": true + }), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: None, + }, + ) +@@ -813,19 +816,22 @@ mod tests { + let tool = make_shell_tool(); + let env: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: "hello".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "hello".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 10, + }, + ..Default::default() + }); +- let result = (tool.executor)(serde_json::json!({"command": "echo hello"}), ToolContext { +- env, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let result = (tool.executor)( ++ serde_json::json!({"command": "echo hello"}), ++ ToolContext { ++ env, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + let output = result.unwrap(); + assert!(output.contains("Exit code: 0")); +@@ -840,8 +846,8 @@ mod tests { + let _result = (tool.executor)( + serde_json::json!({"command": "sleep 1", "timeout_ms": 5000}), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: None, + }, + ) +@@ -854,19 +860,22 @@ mod tests { + let tool = make_shell_tool(); + let env: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: String::new(), +- stderr: "error".into(), +- exit_code: 1, +- timed_out: false, ++ stdout: String::new(), ++ stderr: "error".into(), ++ exit_code: 1, ++ timed_out: false, + duration_ms: 10, + }, + ..Default::default() + }); +- let result = (tool.executor)(serde_json::json!({"command": "false"}), ToolContext { +- env, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let result = (tool.executor)( ++ serde_json::json!({"command": "false"}), ++ ToolContext { ++ env, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + let output = result.unwrap(); + assert!(output.contains("Exit code: 1")); +@@ -878,19 +887,22 @@ mod tests { + let tool = make_shell_tool(); + let env: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: String::new(), +- stderr: String::new(), +- exit_code: -1, +- timed_out: true, ++ stdout: String::new(), ++ stderr: String::new(), ++ exit_code: -1, ++ timed_out: true, + duration_ms: 10000, + }, + ..Default::default() + }); +- let result = (tool.executor)(serde_json::json!({"command": "sleep 100"}), ToolContext { +- env, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let result = (tool.executor)( ++ serde_json::json!({"command": "sleep 100"}), ++ ToolContext { ++ env, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + let output = result.unwrap(); + assert!(output.starts_with("Command timed out.\n")); +@@ -906,8 +918,8 @@ mod tests { + let _result = (tool.executor)( + serde_json::json!({"command": "echo $MY_KEY"}), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: Some(tool_env.clone()), + }, + ) +@@ -921,11 +933,14 @@ mod tests { + let tool = make_shell_tool(); + let env = Arc::new(MockSandbox::default()); + let env_clone: Arc = env.clone(); +- let _result = (tool.executor)(serde_json::json!({"command": "echo hello"}), ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let _result = (tool.executor)( ++ serde_json::json!({"command": "echo hello"}), ++ ToolContext { ++ env: env_clone, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + let captured = env.captured_env_vars.lock().unwrap().clone(); + assert_eq!(captured, None); +@@ -936,10 +951,10 @@ mod tests { + let tool = make_web_fetch_tool(None); + let env = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: "fetched content".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "fetched content".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 100, + }, + ..Default::default() +@@ -950,8 +965,8 @@ mod tests { + let _result = (tool.executor)( + serde_json::json!({"url": "https://example.com"}), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: Some(tool_env.clone()), + }, + ) +@@ -970,11 +985,14 @@ mod tests { + ], + ..Default::default() + }); +- let result = (tool.executor)(serde_json::json!({"pattern": "fn"}), ToolContext { +- env, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let result = (tool.executor)( ++ serde_json::json!({"pattern": "fn"}), ++ ToolContext { ++ env, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + let output = result.unwrap(); + assert!(output.contains("src/main.rs:10:fn main()")); +@@ -988,11 +1006,14 @@ mod tests { + glob_results: vec!["src/main.rs".into(), "src/lib.rs".into()], + ..Default::default() + }); +- let result = (tool.executor)(serde_json::json!({"pattern": "src/**/*.rs"}), ToolContext { +- env, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let result = (tool.executor)( ++ serde_json::json!({"pattern": "src/**/*.rs"}), ++ ToolContext { ++ env, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + let output = result.unwrap(); + assert!(output.contains("src/main.rs")); +@@ -1003,11 +1024,14 @@ mod tests { + async fn web_search_missing_api_key_returns_error() { + let tool = make_web_search_tool_with_api_key(None); + let env: Arc = Arc::new(MockSandbox::default()); +- let result = (tool.executor)(serde_json::json!({"query": "test"}), ToolContext { +- env, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let result = (tool.executor)( ++ serde_json::json!({"query": "test"}), ++ ToolContext { ++ env, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + let err = result.unwrap_err(); + assert!( +@@ -1020,11 +1044,14 @@ mod tests { + async fn web_search_missing_query_returns_error() { + let tool = make_web_search_tool_with_api_key(Some("fake-key".into())); + let env: Arc = Arc::new(MockSandbox::default()); +- let result = (tool.executor)(serde_json::json!({}), ToolContext { +- env, +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ let result = (tool.executor)( ++ serde_json::json!({}), ++ ToolContext { ++ env, ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await; + let err = result.unwrap_err(); + assert!( +@@ -1061,10 +1088,10 @@ mod tests { + let tool = make_web_fetch_tool(None); + let env = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: "

hello

".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "

hello

".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 100, + }, + ..Default::default() +@@ -1073,8 +1100,8 @@ mod tests { + let result = (tool.executor)( + serde_json::json!({"url": "https://example.com"}), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: None, + }, + ) +@@ -1131,8 +1158,8 @@ mod tests { + let _result = (tool.executor)( + serde_json::json!({"url": "https://example.com", "timeout_ms": 15000}), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: None, + }, + ) +@@ -1153,8 +1180,8 @@ mod tests { + let _result = (tool.executor)( + serde_json::json!({"url": "https://example.com", "timeout_ms": 120_000}), + ToolContext { +- env: env_clone, +- cancel: CancellationToken::new(), ++ env: env_clone, ++ cancel: CancellationToken::new(), + tool_env: None, + }, + ) +@@ -1173,10 +1200,10 @@ mod tests { + let tool = make_web_fetch_tool(None); + let env: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: large_content, +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: large_content, ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 100, + }, + ..Default::default() +@@ -1200,10 +1227,10 @@ mod tests { + let tool = make_web_fetch_tool(None); + let env: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: String::new(), +- stderr: "curl: (6) Could not resolve host".into(), +- exit_code: 6, +- timed_out: false, ++ stdout: String::new(), ++ stderr: "curl: (6) Could not resolve host".into(), ++ exit_code: 6, ++ timed_out: false, + duration_ms: 100, + }, + ..Default::default() +@@ -1240,18 +1267,17 @@ mod tests { + client, + model_id: ModelHandle::ByName { + provider: fabro_model::Provider::Anthropic, +- model: "mock-model".to_string(), ++ model: "mock-model".to_string(), + }, + }; + + let tool = make_web_fetch_tool(Some(summarizer)); + let env: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: "

Lots of content about Rust...

" +- .into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "

Lots of content about Rust...

".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 100, + }, + ..Default::default() +@@ -1277,12 +1303,11 @@ mod tests { + let tool = make_web_fetch_tool(None); + let env: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: +- "

Rust is a systems programming language.

" +- .into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "

Rust is a systems programming language.

" ++ .into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 100, + }, + ..Default::default() +@@ -1317,7 +1342,7 @@ mod tests { + // "other_provider" is the default — it rejects all requests. + let default_provider: Arc = Arc::new(MockErrorProvider { + error: LlmError::Provider { +- kind: ProviderErrorKind::NotFound, ++ kind: ProviderErrorKind::NotFound, + detail: Box::new(ProviderErrorDetail::new( + "model not found", + "other_provider", +@@ -1341,17 +1366,17 @@ mod tests { + client, + model_id: ModelHandle::ByName { + provider: fabro_model::Provider::Anthropic, +- model: "target-model".to_string(), ++ model: "target-model".to_string(), + }, + }; + + let tool = make_web_fetch_tool(Some(summarizer)); + let env: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: "

Page content

".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "

Page content

".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 100, + }, + ..Default::default() +@@ -1434,11 +1459,14 @@ mod tests { + + // read_file tool should mark the file as agent-read + let tool = make_read_file_tool(); +- (tool.executor)(serde_json::json!({"file_path": "a.ts"}), ToolContext { +- env: Arc::clone(&env), +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ (tool.executor)( ++ serde_json::json!({"file_path": "a.ts"}), ++ ToolContext { ++ env: Arc::clone(&env), ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await + .unwrap(); + +@@ -1463,11 +1491,14 @@ mod tests { + + // grep tool should mark matched files as agent-read + let tool = make_grep_tool(); +- (tool.executor)(serde_json::json!({"pattern": "content"}), ToolContext { +- env: Arc::clone(&env), +- cancel: CancellationToken::new(), +- tool_env: None, +- }) ++ (tool.executor)( ++ serde_json::json!({"pattern": "content"}), ++ ToolContext { ++ env: Arc::clone(&env), ++ cancel: CancellationToken::new(), ++ tool_env: None, ++ }, ++ ) + .await + .unwrap(); + +diff --git a/lib/crates/fabro-agent/src/types.rs b/lib/crates/fabro-agent/src/types.rs +index 965c98ee..6d4532db 100644 +--- a/lib/crates/fabro-agent/src/types.rs ++++ b/lib/crates/fabro-agent/src/types.rs +@@ -34,36 +34,36 @@ mod system_time_iso8601 { + #[derive(Debug, Clone)] + pub enum Turn { + User { +- content: String, ++ content: String, + timestamp: SystemTime, + }, + Assistant { +- content: String, +- tool_calls: Vec, ++ content: String, ++ tool_calls: Vec, + /// Provider-specific content parts (e.g. `OpenAI` reasoning items, + /// `Anthropic` thinking blocks with signatures) preserved for + /// round-tripping. Reasoning/thinking text is stored here as + /// `ContentPart::Thinking`. + provider_parts: Vec, +- usage: Box, +- response_id: String, +- timestamp: SystemTime, ++ usage: Box, ++ response_id: String, ++ timestamp: SystemTime, + }, + ToolResults { +- results: Vec, ++ results: Vec, + timestamp: SystemTime, + }, + /// Injected content sent as a system-role message to the LLM (maps to + /// `Role::System`). + System { +- content: String, ++ content: String, + timestamp: SystemTime, + }, + /// Injected steering content sent as a user-role message to the LLM (maps + /// to `Role::User`). Used to guide the assistant's behavior + /// mid-conversation without appearing as actual user input. + Steering { +- content: String, ++ content: String, + timestamp: SystemTime, + }, + } +@@ -101,7 +101,7 @@ pub enum AgentEvent { + #[serde(default, skip_serializing_if = "Option::is_none")] + provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- model: Option, ++ model: Option, + }, + SessionEnded, + ProcessingEnd, +@@ -111,14 +111,14 @@ pub enum AgentEvent { + AssistantTextStart, + /// Replaces the current in-progress assistant output buffers. + AssistantOutputReplace { +- text: String, ++ text: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + reasoning: Option, + }, + AssistantMessage { +- text: String, +- model: String, +- usage: TokenCounts, ++ text: String, ++ model: String, ++ usage: TokenCounts, + tool_call_count: usize, + }, + TextDelta { +@@ -128,24 +128,24 @@ pub enum AgentEvent { + delta: String, + }, + ToolCallStarted { +- tool_name: String, ++ tool_name: String, + tool_call_id: String, +- arguments: serde_json::Value, ++ arguments: serde_json::Value, + }, + ToolCallOutputDelta { + delta: String, + }, + ToolCallCompleted { +- tool_name: String, ++ tool_name: String, + tool_call_id: String, +- output: serde_json::Value, +- is_error: bool, ++ output: serde_json::Value, ++ is_error: bool, + }, + Error { + error: Error, + }, + Warning { +- kind: String, ++ kind: String, + message: String, + details: serde_json::Value, + }, +@@ -160,49 +160,49 @@ pub enum AgentEvent { + text: String, + }, + CompactionStarted { +- estimated_tokens: usize, ++ estimated_tokens: usize, + context_window_size: usize, + }, + CompactionCompleted { +- original_turn_count: usize, +- preserved_turn_count: usize, ++ original_turn_count: usize, ++ preserved_turn_count: usize, + summary_token_estimate: usize, +- tracked_file_count: usize, ++ tracked_file_count: usize, + }, + LlmRetry { +- provider: String, +- model: String, +- attempt: usize, ++ provider: String, ++ model: String, ++ attempt: usize, + delay_secs: f64, +- error: LlmError, ++ error: LlmError, + }, + SubAgentSpawned { + agent_id: String, +- depth: usize, +- task: String, ++ depth: usize, ++ task: String, + }, + SubAgentCompleted { +- agent_id: String, +- depth: usize, +- success: bool, ++ agent_id: String, ++ depth: usize, ++ success: bool, + turns_used: usize, + }, + SubAgentFailed { + agent_id: String, +- depth: usize, +- error: Error, ++ depth: usize, ++ error: Error, + }, + SubAgentClosed { + agent_id: String, +- depth: usize, ++ depth: usize, + }, + McpServerReady { + server_name: String, +- tool_count: usize, ++ tool_count: usize, + }, + McpServerFailed { + server_name: String, +- error: String, ++ error: String, + }, + } + +@@ -412,10 +412,10 @@ impl AgentEvent { + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct SessionEvent { +- pub event: AgentEvent, ++ pub event: AgentEvent, + #[serde(with = "system_time_iso8601")] +- pub timestamp: SystemTime, +- pub session_id: String, ++ pub timestamp: SystemTime, ++ pub session_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_session_id: Option, + } +@@ -427,18 +427,21 @@ mod tests { + #[test] + fn session_event_construction() { + let event = SessionEvent { +- event: AgentEvent::SessionStarted { ++ event: AgentEvent::SessionStarted { + provider: Some("anthropic".into()), +- model: Some("claude-opus".into()), ++ model: Some("claude-opus".into()), + }, +- timestamp: SystemTime::now(), +- session_id: "sess_1".into(), ++ timestamp: SystemTime::now(), ++ session_id: "sess_1".into(), + parent_session_id: None, + }; +- assert!(matches!(event.event, AgentEvent::SessionStarted { +- provider: Some(_), +- model: Some(_), +- })); ++ assert!(matches!( ++ event.event, ++ AgentEvent::SessionStarted { ++ provider: Some(_), ++ model: Some(_), ++ } ++ )); + assert_eq!(event.session_id, "sess_1"); + assert_eq!(event.parent_session_id, None); + } +@@ -446,24 +449,30 @@ mod tests { + #[test] + fn compaction_events_constructible() { + let started = AgentEvent::CompactionStarted { +- estimated_tokens: 5000, ++ estimated_tokens: 5000, + context_window_size: 8000, + }; +- assert!(matches!(started, AgentEvent::CompactionStarted { +- estimated_tokens: 5000, +- .. +- })); ++ assert!(matches!( ++ started, ++ AgentEvent::CompactionStarted { ++ estimated_tokens: 5000, ++ .. ++ } ++ )); + + let completed = AgentEvent::CompactionCompleted { +- original_turn_count: 20, +- preserved_turn_count: 6, ++ original_turn_count: 20, ++ preserved_turn_count: 6, + summary_token_estimate: 500, +- tracked_file_count: 3, ++ tracked_file_count: 3, + }; +- assert!(matches!(completed, AgentEvent::CompactionCompleted { +- original_turn_count: 20, +- .. +- })); ++ assert!(matches!( ++ completed, ++ AgentEvent::CompactionCompleted { ++ original_turn_count: 20, ++ .. ++ } ++ )); + } + + #[test] +@@ -480,36 +489,39 @@ mod tests { + fn subagent_spawned_constructible() { + let event = AgentEvent::SubAgentSpawned { + agent_id: "sa-1".into(), +- depth: 1, +- task: "list files".into(), +- }; +- assert!(matches!(event, AgentEvent::SubAgentSpawned { + depth: 1, +- .. +- })); ++ task: "list files".into(), ++ }; ++ assert!(matches!( ++ event, ++ AgentEvent::SubAgentSpawned { depth: 1, .. } ++ )); + } + + #[test] + fn subagent_completed_constructible() { + let event = AgentEvent::SubAgentCompleted { +- agent_id: "sa-1".into(), +- depth: 1, +- success: true, +- turns_used: 5, +- }; +- assert!(matches!(event, AgentEvent::SubAgentCompleted { ++ agent_id: "sa-1".into(), ++ depth: 1, + success: true, + turns_used: 5, +- .. +- })); ++ }; ++ assert!(matches!( ++ event, ++ AgentEvent::SubAgentCompleted { ++ success: true, ++ turns_used: 5, ++ .. ++ } ++ )); + } + + #[test] + fn subagent_failed_constructible() { + let event = AgentEvent::SubAgentFailed { + agent_id: "sa-1".into(), +- depth: 0, +- error: Error::ToolExecution("timeout".into()), ++ depth: 0, ++ error: Error::ToolExecution("timeout".into()), + }; + assert!(matches!(event, AgentEvent::SubAgentFailed { depth: 0, .. })); + } +@@ -518,7 +530,7 @@ mod tests { + fn subagent_closed_constructible() { + let event = AgentEvent::SubAgentClosed { + agent_id: "sa-1".into(), +- depth: 2, ++ depth: 2, + }; + assert!(matches!(event, AgentEvent::SubAgentClosed { depth: 2, .. })); + } +@@ -528,23 +540,23 @@ mod tests { + let events = vec![ + AgentEvent::SubAgentSpawned { + agent_id: "sa-1".into(), +- depth: 0, +- task: "test".into(), ++ depth: 0, ++ task: "test".into(), + }, + AgentEvent::SubAgentCompleted { +- agent_id: "sa-1".into(), +- depth: 0, +- success: true, ++ agent_id: "sa-1".into(), ++ depth: 0, ++ success: true, + turns_used: 3, + }, + AgentEvent::SubAgentFailed { + agent_id: "sa-1".into(), +- depth: 0, +- error: Error::ToolExecution("oops".into()), ++ depth: 0, ++ error: Error::ToolExecution("oops".into()), + }, + AgentEvent::SubAgentClosed { + agent_id: "sa-1".into(), +- depth: 0, ++ depth: 0, + }, + ]; + let json = serde_json::to_string(&events).unwrap(); +@@ -555,12 +567,12 @@ mod tests { + #[test] + fn session_event_serde_round_trip_without_parent_session_id() { + let event = SessionEvent { +- event: AgentEvent::SessionStarted { ++ event: AgentEvent::SessionStarted { + provider: Some("anthropic".into()), +- model: Some("claude-opus".into()), ++ model: Some("claude-opus".into()), + }, +- timestamp: SystemTime::now(), +- session_id: "sess_42".into(), ++ timestamp: SystemTime::now(), ++ session_id: "sess_42".into(), + parent_session_id: None, + }; + let json = serde_json::to_string(&event).unwrap(); +@@ -573,21 +585,24 @@ mod tests { + let deserialized: SessionEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.session_id, "sess_42"); + assert_eq!(deserialized.parent_session_id, None); +- assert!(matches!(deserialized.event, AgentEvent::SessionStarted { +- provider: Some(_), +- model: Some(_), +- })); ++ assert!(matches!( ++ deserialized.event, ++ AgentEvent::SessionStarted { ++ provider: Some(_), ++ model: Some(_), ++ } ++ )); + } + + #[test] + fn session_event_serde_round_trip_with_parent_session_id() { + let event = SessionEvent { +- event: AgentEvent::SessionStarted { ++ event: AgentEvent::SessionStarted { + provider: Some("openai".into()), +- model: Some("gpt-5.4".into()), ++ model: Some("gpt-5.4".into()), + }, +- timestamp: SystemTime::now(), +- session_id: "sess_child".into(), ++ timestamp: SystemTime::now(), ++ session_id: "sess_child".into(), + parent_session_id: Some("sess_parent".into()), + }; + let json = serde_json::to_string(&event).unwrap(); +@@ -606,19 +621,19 @@ mod tests { + fn mcp_server_ready_constructible() { + let event = AgentEvent::McpServerReady { + server_name: "filesystem".into(), +- tool_count: 3, +- }; +- assert!(matches!(event, AgentEvent::McpServerReady { + tool_count: 3, +- .. +- })); ++ }; ++ assert!(matches!( ++ event, ++ AgentEvent::McpServerReady { tool_count: 3, .. } ++ )); + } + + #[test] + fn mcp_server_failed_constructible() { + let event = AgentEvent::McpServerFailed { + server_name: "broken".into(), +- error: "connection refused".into(), ++ error: "connection refused".into(), + }; + assert!( + matches!(event, AgentEvent::McpServerFailed { server_name, .. } if server_name == "broken") +@@ -630,20 +645,20 @@ mod tests { + let events = vec![ + AgentEvent::McpServerReady { + server_name: "fs".into(), +- tool_count: 5, ++ tool_count: 5, + }, + AgentEvent::McpServerFailed { + server_name: "bad".into(), +- error: "timeout".into(), ++ error: "timeout".into(), + }, + ]; + let json = serde_json::to_string(&events).unwrap(); + let deserialized: Vec = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.len(), 2); +- assert!(matches!(&deserialized[0], AgentEvent::McpServerReady { +- tool_count: 5, +- .. +- })); ++ assert!(matches!( ++ &deserialized[0], ++ AgentEvent::McpServerReady { tool_count: 5, .. } ++ )); + assert!(matches!( + &deserialized[1], + AgentEvent::McpServerFailed { .. } +@@ -653,16 +668,16 @@ mod tests { + #[test] + fn agent_event_assistant_message() { + let usage = TokenCounts { +- input_tokens: 100, +- output_tokens: 50, +- cache_read_tokens: 80, ++ input_tokens: 100, ++ output_tokens: 50, ++ cache_read_tokens: 80, + cache_write_tokens: 10, +- reasoning_tokens: 20, ++ reasoning_tokens: 20, + }; + let event = AgentEvent::AssistantMessage { +- text: "Hello".into(), +- model: "test-model".into(), +- usage: usage.clone(), ++ text: "Hello".into(), ++ model: "test-model".into(), ++ usage: usage.clone(), + tool_call_count: 2, + }; + match &event { +@@ -683,7 +698,7 @@ mod tests { + #[test] + fn agent_event_assistant_output_replace_roundtrip() { + let event = AgentEvent::AssistantOutputReplace { +- text: "Hello again".into(), ++ text: "Hello again".into(), + reasoning: Some("Retrying from scratch".into()), + }; + let json = serde_json::to_string(&event).unwrap(); +@@ -704,7 +719,7 @@ mod tests { + let event = AgentEvent::Error { + error: Error::Llm(LlmError::Network { + message: "refused".into(), +- source: None, ++ source: None, + }), + }; + let json = serde_json::to_string(&event).unwrap(); +@@ -721,19 +736,19 @@ mod tests { + fn llm_retry_event_carries_sdk_error() { + use fabro_llm::error::{ProviderErrorDetail, ProviderErrorKind}; + let event = AgentEvent::LlmRetry { +- provider: "openai".into(), +- model: "gpt-4".into(), +- attempt: 1, ++ provider: "openai".into(), ++ model: "gpt-4".into(), ++ attempt: 1, + delay_secs: 2.0, +- error: LlmError::Provider { +- kind: ProviderErrorKind::RateLimit, ++ error: LlmError::Provider { ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail { +- message: "too fast".into(), +- provider: "openai".into(), ++ message: "too fast".into(), ++ provider: "openai".into(), + status_code: Some(429), +- error_code: None, ++ error_code: None, + retry_after: Some(2.0), +- raw: None, ++ raw: None, + }), + }, + }; +@@ -752,8 +767,8 @@ mod tests { + fn subagent_failed_carries_agent_error() { + let event = AgentEvent::SubAgentFailed { + agent_id: "sa-1".into(), +- depth: 0, +- error: Error::ToolExecution("cmd failed".into()), ++ depth: 0, ++ error: Error::ToolExecution("cmd failed".into()), + }; + let json = serde_json::to_string(&event).unwrap(); + let deserialized: AgentEvent = serde_json::from_str(&json).unwrap(); +@@ -780,7 +795,7 @@ mod tests { + fn mcp_server_failed_still_string() { + let event = AgentEvent::McpServerFailed { + server_name: "broken".into(), +- error: "connection refused".into(), ++ error: "connection refused".into(), + }; + let json = serde_json::to_string(&event).unwrap(); + let deserialized: AgentEvent = serde_json::from_str(&json).unwrap(); +diff --git a/lib/crates/fabro-agent/src/v4a_patch.rs b/lib/crates/fabro-agent/src/v4a_patch.rs +index eb45971b..eb63aa9d 100644 +--- a/lib/crates/fabro-agent/src/v4a_patch.rs ++++ b/lib/crates/fabro-agent/src/v4a_patch.rs +@@ -16,23 +16,23 @@ pub enum Change { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Hunk { + pub context_line: String, +- pub changes: Vec, +- pub end_of_file: bool, ++ pub changes: Vec, ++ pub end_of_file: bool, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum PatchOperation { + Add { +- path: String, ++ path: String, + content: String, + }, + Delete { + path: String, + }, + Update { +- path: String, ++ path: String, + new_path: Option, +- hunks: Vec, ++ hunks: Vec, + }, + } + +@@ -402,9 +402,9 @@ fn format_patch_error(error: &str, path: &str, content: &str) -> String { + pub fn make_apply_patch_tool() -> RegisteredTool { + RegisteredTool { + definition: ToolDefinition { +- name: "apply_patch".into(), ++ name: "apply_patch".into(), + description: "Apply a v4a format patch to modify files".into(), +- parameters: serde_json::json!({ ++ parameters: serde_json::json!({ + "type": "object", + "properties": { + "patch": { +@@ -415,7 +415,7 @@ pub fn make_apply_patch_tool() -> RegisteredTool { + "required": ["patch"] + }), + }, +- executor: Arc::new(|args, ctx| { ++ executor: Arc::new(|args, ctx| { + Box::pin(async move { + let patch_text = args + .get("patch") +@@ -448,10 +448,13 @@ mod tests { + + let ops = parse_v4a_patch(patch).unwrap(); + assert_eq!(ops.len(), 1); +- assert_eq!(ops[0], PatchOperation::Add { +- path: "src/new_file.rs".into(), +- content: "fn main() {\n println!(\"hello\");\n}".into(), +- }); ++ assert_eq!( ++ ops[0], ++ PatchOperation::Add { ++ path: "src/new_file.rs".into(), ++ content: "fn main() {\n println!(\"hello\");\n}".into(), ++ } ++ ); + } + + #[test] +@@ -463,9 +466,12 @@ mod tests { + + let ops = parse_v4a_patch(patch).unwrap(); + assert_eq!(ops.len(), 1); +- assert_eq!(ops[0], PatchOperation::Delete { +- path: "src/old_file.rs".into(), +- }); ++ assert_eq!( ++ ops[0], ++ PatchOperation::Delete { ++ path: "src/old_file.rs".into(), ++ } ++ ); + } + + #[test] +@@ -582,21 +588,21 @@ mod tests { + let env = MutableMockSandbox::new(files); + + let ops = vec![PatchOperation::Update { +- path: "src/game.py".into(), ++ path: "src/game.py".into(), + new_path: None, +- hunks: vec![ ++ hunks: vec![ + Hunk { + context_line: String::new(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove("from src.cards import Suit".into()), + Change::Add("from src.cards import Card, Suit".into()), + ], + }, + Hunk { + context_line: String::new(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove(" stock: list = field(default_factory=list)".into()), + Change::Remove(" waste: list = field(default_factory=list)".into()), + Change::Add(" stock: list[Card] = field(default_factory=list)".into()), +@@ -715,12 +721,12 @@ mod tests { + let env = MutableMockSandbox::new(files); + + let ops = vec![PatchOperation::Update { +- path: "src/lib.rs".into(), ++ path: "src/lib.rs".into(), + new_path: None, +- hunks: vec![Hunk { ++ hunks: vec![Hunk { + context_line: String::new(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Context("fn unchanged() {".into()), + Change::Remove(" old_line();".into()), + Change::Add(" new_line();".into()), +@@ -746,21 +752,21 @@ mod tests { + let env = MutableMockSandbox::new(files); + + let ops = vec![PatchOperation::Update { +- path: "src/lib.rs".into(), ++ path: "src/lib.rs".into(), + new_path: None, +- hunks: vec![ ++ hunks: vec![ + Hunk { + context_line: "def setup():".into(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove(" old_setup()".into()), + Change::Add(" new_setup()".into()), + ], + }, + Hunk { + context_line: String::new(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove(" old_teardown()".into()), + Change::Add(" new_teardown()".into()), + ], +@@ -782,7 +788,7 @@ mod tests { + async fn apply_patch_add_file() { + let env = MutableMockSandbox::new(HashMap::new()); + let ops = vec![PatchOperation::Add { +- path: "src/new.rs".into(), ++ path: "src/new.rs".into(), + content: "fn new() {}".into(), + }]; + +@@ -803,12 +809,12 @@ mod tests { + let env = MutableMockSandbox::new(files); + + let ops = vec![PatchOperation::Update { +- path: "src/lib.rs".into(), ++ path: "src/lib.rs".into(), + new_path: None, +- hunks: vec![Hunk { ++ hunks: vec![Hunk { + context_line: "fn hello() {".into(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove(" println!(\"old\");".into()), + Change::Add(" println!(\"new\");".into()), + ], +@@ -856,12 +862,12 @@ mod tests { + let env = MutableMockSandbox::new(files); + + let ops = vec![PatchOperation::Update { +- path: "src/game.py".into(), ++ path: "src/game.py".into(), + new_path: None, +- hunks: vec![Hunk { ++ hunks: vec![Hunk { + context_line: "def nonexistent():".into(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove(" old_body()".into()), + Change::Add(" new_body()".into()), + ], +@@ -882,16 +888,16 @@ mod tests { + let hunks = vec![ + Hunk { + context_line: String::new(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove(" pass".into()), + Change::Add(" return 1".into()), + ], + }, + Hunk { + context_line: String::new(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove(" pass".into()), + Change::Add(" return 2".into()), + ], +@@ -976,8 +982,8 @@ mod tests { + let content = "def foo():\n pass\n\ndef bar():\n pass"; + let hunks = vec![Hunk { + context_line: String::new(), +- end_of_file: true, +- changes: vec![ ++ end_of_file: true, ++ changes: vec![ + Change::Remove(" pass".into()), + Change::Add(" return 99".into()), + ], +@@ -1025,12 +1031,12 @@ mod tests { + let env = MutableMockSandbox::new(files); + + let ops = vec![PatchOperation::Update { +- path: "src/old.py".into(), ++ path: "src/old.py".into(), + new_path: Some("src/new.py".into()), +- hunks: vec![Hunk { ++ hunks: vec![Hunk { + context_line: "def hello():".into(), +- end_of_file: false, +- changes: vec![ ++ end_of_file: false, ++ changes: vec![ + Change::Remove(" pass".into()), + Change::Add(" return 1".into()), + ], +@@ -1057,8 +1063,8 @@ mod tests { + let content = " indented\nindented"; + let hunks = vec![Hunk { + context_line: "indented".into(), +- end_of_file: false, +- changes: vec![Change::Add("extra".into())], ++ end_of_file: false, ++ changes: vec![Change::Add("extra".into())], + }]; + let result = apply_hunks(content, &hunks).unwrap(); + // Should match line 1 (exact), so "extra" inserted after "indented" (line 1) +@@ -1070,8 +1076,8 @@ mod tests { + let content = "print(\u{201C}hello\u{201D})"; + let hunks = vec![Hunk { + context_line: "print(\"hello\")".into(), +- end_of_file: false, +- changes: vec![Change::Add("print(\"world\")".into())], ++ end_of_file: false, ++ changes: vec![Change::Add("print(\"world\")".into())], + }]; + let result = apply_hunks(content, &hunks).unwrap(); + // Original line preserved, new line added after +diff --git a/lib/crates/fabro-agent/tests/it/parity_matrix.rs b/lib/crates/fabro-agent/tests/it/parity_matrix.rs +index 51d3b731..69668efe 100644 +--- a/lib/crates/fabro-agent/tests/it/parity_matrix.rs ++++ b/lib/crates/fabro-agent/tests/it/parity_matrix.rs +@@ -18,7 +18,7 @@ use tokio::sync::Mutex as AsyncMutex; + #[derive(Clone)] + struct OpenAiTwinOptions { + base_url: String, +- api_key: String, ++ api_key: String, + } + + fn summarizer_model_id(provider: Provider) -> ModelHandle { +@@ -30,22 +30,22 @@ fn summarizer_model_id(provider: Provider) -> ModelHandle { + | Provider::Inception + | Provider::OpenAiCompatible => ModelHandle::ByName { + provider: Provider::OpenAi, +- model: "gpt-5.4-mini".to_string(), ++ model: "gpt-5.4-mini".to_string(), + }, + Provider::Gemini => ModelHandle::ByName { + provider: Provider::Gemini, +- model: "gemini-3-flash-preview".to_string(), ++ model: "gemini-3-flash-preview".to_string(), + }, + Provider::Anthropic => ModelHandle::ByName { + provider: Provider::Anthropic, +- model: "claude-haiku-4-5".to_string(), ++ model: "claude-haiku-4-5".to_string(), + }, + } + } + + fn build_summarizer(provider: Provider, client: &Client) -> WebFetchSummarizer { + WebFetchSummarizer { +- client: client.clone(), ++ client: client.clone(), + model_id: summarizer_model_id(provider), + } + } +diff --git a/lib/crates/fabro-auth/src/context.rs b/lib/crates/fabro-auth/src/context.rs +index 93bf14c4..2bc56b46 100644 +--- a/lib/crates/fabro-auth/src/context.rs ++++ b/lib/crates/fabro-auth/src/context.rs +@@ -3,13 +3,13 @@ use fabro_model::Provider; + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum AuthContextRequest { + ApiKey { +- provider: Provider, ++ provider: Provider, + env_var_names: Vec, + }, + DeviceCode { +- user_code: String, ++ user_code: String, + verification_uri: String, +- expires_in: u64, ++ expires_in: u64, + }, + } + +diff --git a/lib/crates/fabro-auth/src/credential.rs b/lib/crates/fabro-auth/src/credential.rs +index ac5479a7..2b244a66 100644 +--- a/lib/crates/fabro-auth/src/credential.rs ++++ b/lib/crates/fabro-auth/src/credential.rs +@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; + pub struct AuthCredential { + pub provider: Provider, + #[serde(flatten)] +- pub details: AuthDetails, ++ pub details: AuthDetails, + } + + impl AuthCredential { +@@ -28,8 +28,8 @@ pub enum AuthDetails { + key: String, + }, + CodexOAuth { +- tokens: OAuthTokens, +- config: OAuthConfig, ++ tokens: OAuthTokens, ++ config: OAuthConfig, + #[serde(default, skip_serializing_if = "Option::is_none")] + account_id: Option, + }, +@@ -37,9 +37,9 @@ pub enum AuthDetails { + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + pub struct OAuthTokens { +- pub access_token: String, ++ pub access_token: String, + pub refresh_token: Option, +- pub expires_at: DateTime, ++ pub expires_at: DateTime, + } + + pub(crate) fn expires_at_from_now(expires_in: Option) -> DateTime { +@@ -49,12 +49,12 @@ pub(crate) fn expires_at_from_now(expires_in: Option) -> DateTime { + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + pub struct OAuthConfig { +- pub auth_url: String, +- pub token_url: String, +- pub client_id: String, +- pub scopes: Vec, ++ pub auth_url: String, ++ pub token_url: String, ++ pub client_id: String, ++ pub scopes: Vec, + pub redirect_uri: Option, +- pub use_pkce: bool, ++ pub use_pkce: bool, + } + + #[derive(Debug, Clone, PartialEq, Eq)] +@@ -94,19 +94,19 @@ mod tests { + fn oauth_credential(expires_at: DateTime) -> AuthCredential { + AuthCredential { + provider: Provider::OpenAi, +- details: AuthDetails::CodexOAuth { +- tokens: OAuthTokens { ++ details: AuthDetails::CodexOAuth { ++ tokens: OAuthTokens { + access_token: "access".to_string(), + refresh_token: Some("refresh".to_string()), + expires_at, + }, +- config: OAuthConfig { +- auth_url: "https://auth.openai.com".to_string(), +- token_url: "https://auth.openai.com/oauth/token".to_string(), +- client_id: "client".to_string(), +- scopes: vec!["openid".to_string()], ++ config: OAuthConfig { ++ auth_url: "https://auth.openai.com".to_string(), ++ token_url: "https://auth.openai.com/oauth/token".to_string(), ++ client_id: "client".to_string(), ++ scopes: vec!["openid".to_string()], + redirect_uri: Some("https://auth.openai.com/deviceauth/callback".to_string()), +- use_pkce: true, ++ use_pkce: true, + }, + account_id: Some("acct_123".to_string()), + }, +@@ -137,7 +137,7 @@ mod tests { + fn credential_id_for_openai_api_key() { + let credential = AuthCredential { + provider: Provider::OpenAi, +- details: AuthDetails::ApiKey { ++ details: AuthDetails::ApiKey { + key: "sk-test".to_string(), + }, + }; +diff --git a/lib/crates/fabro-auth/src/refresh.rs b/lib/crates/fabro-auth/src/refresh.rs +index c1eb04be..5ef7f4e1 100644 +--- a/lib/crates/fabro-auth/src/refresh.rs ++++ b/lib/crates/fabro-auth/src/refresh.rs +@@ -25,15 +25,15 @@ pub async fn refresh_oauth_credential( + .map_err(anyhow::Error::msg)?; + Ok(AuthCredential { + provider: credential.provider, +- details: AuthDetails::CodexOAuth { +- tokens: OAuthTokens { +- access_token: response.access_token, ++ details: AuthDetails::CodexOAuth { ++ tokens: OAuthTokens { ++ access_token: response.access_token, + refresh_token: response + .refresh_token + .or_else(|| tokens.refresh_token.clone()), +- expires_at: expires_at_from_now(response.expires_in), ++ expires_at: expires_at_from_now(response.expires_in), + }, +- config: config.clone(), ++ config: config.clone(), + account_id: account_id.clone(), + }, + }) +diff --git a/lib/crates/fabro-auth/src/resolve.rs b/lib/crates/fabro-auth/src/resolve.rs +index b4136ba3..4ca2e447 100644 +--- a/lib/crates/fabro-auth/src/resolve.rs ++++ b/lib/crates/fabro-auth/src/resolve.rs +@@ -28,18 +28,18 @@ pub enum CredentialUsage { + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ApiCredential { +- pub provider: Provider, +- pub auth_header: ApiKeyHeader, ++ pub provider: Provider, ++ pub auth_header: ApiKeyHeader, + pub extra_headers: HashMap, +- pub base_url: Option, +- pub codex_mode: bool, +- pub org_id: Option, +- pub project_id: Option, ++ pub base_url: Option, ++ pub codex_mode: bool, ++ pub org_id: Option, ++ pub project_id: Option, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct CliCredential { +- pub env_vars: HashMap, ++ pub env_vars: HashMap, + pub login_command: Option, + } + +@@ -57,7 +57,7 @@ pub enum ResolveError { + RefreshFailed { + provider: Provider, + #[source] +- source: anyhow::Error, ++ source: anyhow::Error, + }, + #[error("{0} requires re-authentication: missing refresh token")] + RefreshTokenMissing(Provider), +@@ -65,7 +65,7 @@ pub enum ResolveError { + + #[derive(Clone)] + pub struct CredentialResolver { +- vault: Arc>, ++ vault: Arc>, + env_lookup: EnvLookup, + } + +@@ -200,7 +200,7 @@ impl CredentialResolver { + provider: credential.provider, + auth_header: match credential.provider { + Provider::Anthropic => ApiKeyHeader::Custom { +- name: "x-api-key".to_string(), ++ name: "x-api-key".to_string(), + value: key.clone(), + }, + _ => ApiKeyHeader::Bearer(key.clone()), +@@ -349,13 +349,13 @@ mod tests { + fn oauth_credential(token_url: String, expires_at: chrono::DateTime) -> AuthCredential { + AuthCredential { + provider: Provider::OpenAi, +- details: AuthDetails::CodexOAuth { +- tokens: OAuthTokens { ++ details: AuthDetails::CodexOAuth { ++ tokens: OAuthTokens { + access_token: "expired-access".to_string(), + refresh_token: Some("refresh-token".to_string()), + expires_at, + }, +- config: OAuthConfig { ++ config: OAuthConfig { + auth_url: "https://auth.openai.com".to_string(), + token_url, + client_id: "test-client".to_string(), +@@ -469,10 +469,13 @@ mod tests { + panic!("expected api credential"); + }; + +- assert_eq!(api.auth_header, ApiKeyHeader::Custom { +- name: "x-api-key".to_string(), +- value: "anthropic-key".to_string(), +- }); ++ assert_eq!( ++ api.auth_header, ++ ApiKeyHeader::Custom { ++ name: "x-api-key".to_string(), ++ value: "anthropic-key".to_string(), ++ } ++ ); + } + + #[tokio::test] +@@ -653,9 +656,10 @@ mod tests { + let resolver = test_resolver(vault, Arc::new(|_| None)); + let vault = resolver.vault.read().await; + +- assert_eq!(resolver.configured_providers(&vault), vec![ +- Provider::OpenAi +- ]); ++ assert_eq!( ++ resolver.configured_providers(&vault), ++ vec![Provider::OpenAi] ++ ); + } + + #[tokio::test] +@@ -668,9 +672,10 @@ mod tests { + ); + let vault = resolver.vault.read().await; + +- assert_eq!(resolver.configured_providers(&vault), vec![ +- Provider::OpenAi +- ]); ++ assert_eq!( ++ resolver.configured_providers(&vault), ++ vec![Provider::OpenAi] ++ ); + } + + #[tokio::test] +diff --git a/lib/crates/fabro-auth/src/strategies/api_key.rs b/lib/crates/fabro-auth/src/strategies/api_key.rs +index 5de78ea6..c5f06290 100644 +--- a/lib/crates/fabro-auth/src/strategies/api_key.rs ++++ b/lib/crates/fabro-auth/src/strategies/api_key.rs +@@ -20,7 +20,7 @@ impl ApiKeyStrategy { + impl AuthStrategy for ApiKeyStrategy { + async fn init(&mut self) -> anyhow::Result { + Ok(AuthContextRequest::ApiKey { +- provider: self.provider, ++ provider: self.provider, + env_var_names: self + .provider + .api_key_env_vars() +@@ -34,7 +34,7 @@ impl AuthStrategy for ApiKeyStrategy { + match response { + AuthContextResponse::ApiKey { key } => Ok(AuthCredential { + provider: self.provider, +- details: AuthDetails::ApiKey { key }, ++ details: AuthDetails::ApiKey { key }, + }), + AuthContextResponse::DeviceCodeConfirmed => { + Err(anyhow::anyhow!("expected API key response")) +diff --git a/lib/crates/fabro-auth/src/strategies/codex_device.rs b/lib/crates/fabro-auth/src/strategies/codex_device.rs +index 3a7197a9..f6d7315f 100644 +--- a/lib/crates/fabro-auth/src/strategies/codex_device.rs ++++ b/lib/crates/fabro-auth/src/strategies/codex_device.rs +@@ -38,9 +38,9 @@ struct JwtPayload { + #[serde(default)] + chatgpt_account_id: Option, + #[serde(default, rename = "https://api.openai.com/auth")] +- auth_claim: Option, ++ auth_claim: Option, + #[serde(default)] +- organizations: Option>, ++ organizations: Option>, + } + + #[derive(Debug, Deserialize)] +@@ -100,48 +100,48 @@ impl U64OrString { + #[derive(Debug, Deserialize)] + struct DeviceCodeInitResponse { + device_auth_id: String, +- user_code: String, ++ user_code: String, + #[serde(default)] +- interval: Option, ++ interval: Option, + #[serde(default)] +- expires_in: Option, ++ expires_in: Option, + #[serde(default)] +- expires_at: Option>, ++ expires_at: Option>, + } + + #[derive(Debug, Deserialize)] + struct DeviceCodePollResponse { + #[serde(default)] +- status: Option, ++ status: Option, + #[serde(default)] + authorization_code: Option, + #[serde(default)] +- code_verifier: Option, ++ code_verifier: Option, + } + + #[derive(Debug, Serialize)] + struct DeviceCodeInitRequest<'a> { + client_id: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] +- scope: Option, ++ scope: Option, + } + + #[derive(Debug)] + struct PendingDeviceAuth { + device_auth_id: String, +- user_code: String, +- poll_interval: Duration, +- deadline: Instant, ++ user_code: String, ++ poll_interval: Duration, ++ deadline: Instant, + } + + #[derive(Debug)] + struct DeviceAuthorization { + authorization_code: String, +- code_verifier: String, ++ code_verifier: String, + } + + pub struct CodexDeviceStrategy { +- config: OAuthConfig, ++ config: OAuthConfig, + pending: Option, + } + +@@ -247,7 +247,7 @@ impl AuthStrategy for CodexDeviceStrategy { + .header("originator", "fabro") + .json(&DeviceCodeInitRequest { + client_id: &self.config.client_id, +- scope: (!self.config.scopes.is_empty()).then(|| self.config.scopes.join(" ")), ++ scope: (!self.config.scopes.is_empty()).then(|| self.config.scopes.join(" ")), + }) + .send() + .await?; +@@ -301,13 +301,13 @@ impl AuthStrategy for CodexDeviceStrategy { + + Ok(AuthCredential { + provider: fabro_model::Provider::OpenAi, +- details: AuthDetails::CodexOAuth { +- tokens: OAuthTokens { +- access_token: token_response.access_token, ++ details: AuthDetails::CodexOAuth { ++ tokens: OAuthTokens { ++ access_token: token_response.access_token, + refresh_token: token_response.refresh_token, +- expires_at: expires_at_from_now(token_response.expires_in), ++ expires_at: expires_at_from_now(token_response.expires_in), + }, +- config: self.config.clone(), ++ config: self.config.clone(), + account_id: token_response + .id_token + .as_deref() +@@ -329,17 +329,17 @@ mod tests { + + fn test_config(server: &MockServer) -> OAuthConfig { + OAuthConfig { +- auth_url: server.url(""), +- token_url: server.url("/oauth/token"), +- client_id: "test-client".to_string(), +- scopes: vec![ ++ auth_url: server.url(""), ++ token_url: server.url("/oauth/token"), ++ client_id: "test-client".to_string(), ++ scopes: vec![ + "openid".to_string(), + "profile".to_string(), + "email".to_string(), + "offline_access".to_string(), + ], + redirect_uri: Some("https://auth.openai.com/deviceauth/callback".to_string()), +- use_pkce: false, ++ use_pkce: false, + } + } + +@@ -415,11 +415,14 @@ mod tests { + + let request = strategy.init().await.unwrap(); + +- assert_eq!(request, AuthContextRequest::DeviceCode { +- user_code: "ABCD-EFGH".to_string(), +- verification_uri: "https://auth.openai.com/codex/device".to_string(), +- expires_in: 300, +- }); ++ assert_eq!( ++ request, ++ AuthContextRequest::DeviceCode { ++ user_code: "ABCD-EFGH".to_string(), ++ verification_uri: "https://auth.openai.com/codex/device".to_string(), ++ expires_in: 300, ++ } ++ ); + init_mock.assert_async().await; + } + +diff --git a/lib/crates/fabro-auth/src/strategy.rs b/lib/crates/fabro-auth/src/strategy.rs +index 1d28afcc..d0ca9f3c 100644 +--- a/lib/crates/fabro-auth/src/strategy.rs ++++ b/lib/crates/fabro-auth/src/strategy.rs +@@ -25,17 +25,17 @@ pub enum AuthMethod { + #[must_use] + pub fn codex_oauth_config() -> OAuthConfig { + OAuthConfig { +- auth_url: CODEX_AUTH_URL.to_string(), +- token_url: CODEX_TOKEN_URL.to_string(), +- client_id: CODEX_CLIENT_ID.to_string(), +- scopes: vec![ ++ auth_url: CODEX_AUTH_URL.to_string(), ++ token_url: CODEX_TOKEN_URL.to_string(), ++ client_id: CODEX_CLIENT_ID.to_string(), ++ scopes: vec![ + "openid".to_string(), + "profile".to_string(), + "email".to_string(), + "offline_access".to_string(), + ], + redirect_uri: Some(format!("{CODEX_AUTH_URL}/deviceauth/callback")), +- use_pkce: false, ++ use_pkce: false, + } + } + +@@ -73,9 +73,12 @@ mod tests { + async fn api_key_strategy_uses_provider_env_names() { + let mut strategy = ApiKeyStrategy::new(Provider::Anthropic); + let request = strategy.init().await.unwrap(); +- assert_eq!(request, AuthContextRequest::ApiKey { +- provider: Provider::Anthropic, +- env_var_names: vec!["ANTHROPIC_API_KEY".to_string()], +- }); ++ assert_eq!( ++ request, ++ AuthContextRequest::ApiKey { ++ provider: Provider::Anthropic, ++ env_var_names: vec!["ANTHROPIC_API_KEY".to_string()], ++ } ++ ); + } + } +diff --git a/lib/crates/fabro-auth/src/vault_ext.rs b/lib/crates/fabro-auth/src/vault_ext.rs +index 99d1c530..fc185cce 100644 +--- a/lib/crates/fabro-auth/src/vault_ext.rs ++++ b/lib/crates/fabro-auth/src/vault_ext.rs +@@ -48,19 +48,19 @@ mod tests { + fn oauth_credential() -> AuthCredential { + AuthCredential { + provider: Provider::OpenAi, +- details: AuthDetails::CodexOAuth { +- tokens: OAuthTokens { +- access_token: "access".to_string(), ++ details: AuthDetails::CodexOAuth { ++ tokens: OAuthTokens { ++ access_token: "access".to_string(), + refresh_token: Some("refresh".to_string()), +- expires_at: Utc::now() + Duration::hours(1), ++ expires_at: Utc::now() + Duration::hours(1), + }, +- config: OAuthConfig { +- auth_url: "https://auth.openai.com".to_string(), +- token_url: "https://auth.openai.com/oauth/token".to_string(), +- client_id: "client".to_string(), +- scopes: vec!["openid".to_string()], ++ config: OAuthConfig { ++ auth_url: "https://auth.openai.com".to_string(), ++ token_url: "https://auth.openai.com/oauth/token".to_string(), ++ client_id: "client".to_string(), ++ scopes: vec!["openid".to_string()], + redirect_uri: Some("https://auth.openai.com/deviceauth/callback".to_string()), +- use_pkce: true, ++ use_pkce: true, + }, + account_id: Some("acct_123".to_string()), + }, +@@ -86,12 +86,16 @@ mod tests { + let dir = tempfile::tempdir().unwrap(); + let mut vault = Vault::load(dir.path().join("secrets.json")).unwrap(); + vault_set_credential(&mut vault, "openai_codex", &oauth_credential()).unwrap(); +- vault_set_credential(&mut vault, "anthropic", &AuthCredential { +- provider: Provider::Anthropic, +- details: AuthDetails::ApiKey { +- key: "anthropic-key".to_string(), ++ vault_set_credential( ++ &mut vault, ++ "anthropic", ++ &AuthCredential { ++ provider: Provider::Anthropic, ++ details: AuthDetails::ApiKey { ++ key: "anthropic-key".to_string(), ++ }, + }, +- }) ++ ) + .unwrap(); + + let credentials = vault_credentials_for_provider(&vault, Provider::OpenAi); +diff --git a/lib/crates/fabro-checkpoint/src/author.rs b/lib/crates/fabro-checkpoint/src/author.rs +index d8e364c6..9cd81540 100644 +--- a/lib/crates/fabro-checkpoint/src/author.rs ++++ b/lib/crates/fabro-checkpoint/src/author.rs +@@ -6,14 +6,14 @@ use fabro_types::settings::run::{GitAuthorLayer, GitAuthorSettings}; + /// Resolved git author identity for checkpoint commits. + #[derive(Debug, Clone, PartialEq)] + pub struct GitAuthor { +- pub name: String, ++ pub name: String, + pub email: String, + } + + impl Default for GitAuthor { + fn default() -> Self { + Self { +- name: "Fabro".into(), ++ name: "Fabro".into(), + email: "noreply@fabro.sh".into(), + } + } +@@ -24,7 +24,7 @@ impl GitAuthor { + pub fn from_options(name: Option, email: Option) -> Self { + let defaults = Self::default(); + Self { +- name: name.unwrap_or(defaults.name), ++ name: name.unwrap_or(defaults.name), + email: email.unwrap_or(defaults.email), + } + } +diff --git a/lib/crates/fabro-checkpoint/src/branch.rs b/lib/crates/fabro-checkpoint/src/branch.rs +index 52958ee8..f275222f 100644 +--- a/lib/crates/fabro-checkpoint/src/branch.rs ++++ b/lib/crates/fabro-checkpoint/src/branch.rs +@@ -7,11 +7,11 @@ use crate::git::{FileMode, Store, TreeEntries}; + /// Metadata about a commit, returned by `log`. + #[derive(Debug)] + pub struct CommitInfo { +- pub oid: Oid, +- pub message: String, +- pub author_name: String, ++ pub oid: Oid, ++ pub message: String, ++ pub author_name: String, + pub author_email: String, +- pub time: git2::Time, ++ pub time: git2::Time, + } + + /// Key-value storage on a single git branch. Each write creates one commit. +@@ -19,8 +19,8 @@ pub struct CommitInfo { + /// the previous. + pub struct BranchStore<'a> { + objects: &'a Store, +- branch: String, +- author: Signature<'static>, ++ branch: String, ++ author: Signature<'static>, + } + + impl<'a> BranchStore<'a> { +diff --git a/lib/crates/fabro-checkpoint/src/error.rs b/lib/crates/fabro-checkpoint/src/error.rs +index 6d59481c..fa5df446 100644 +--- a/lib/crates/fabro-checkpoint/src/error.rs ++++ b/lib/crates/fabro-checkpoint/src/error.rs +@@ -9,7 +9,7 @@ pub enum Error { + + #[error("reading file {path}: {source}")] + ReadFile { +- path: PathBuf, ++ path: PathBuf, + source: std::io::Error, + }, + +diff --git a/lib/crates/fabro-checkpoint/src/git.rs b/lib/crates/fabro-checkpoint/src/git.rs +index 30f4eca6..a60d9aed 100644 +--- a/lib/crates/fabro-checkpoint/src/git.rs ++++ b/lib/crates/fabro-checkpoint/src/git.rs +@@ -34,7 +34,7 @@ impl FileMode { + /// A single entry in a flat tree map. + #[derive(Debug, Clone)] + pub struct TreeEntry { +- pub oid: Oid, ++ pub oid: Oid, + pub filemode: FileMode, + } + +@@ -125,7 +125,7 @@ impl Store { + /// unix. + pub fn write_blob_from_file(&self, path: &Path) -> Result<(Oid, FileMode)> { + let content = std::fs::read(path).map_err(|e| Error::ReadFile { +- path: path.to_path_buf(), ++ path: path.to_path_buf(), + source: e, + })?; + let mode = detect_filemode(path); +@@ -250,14 +250,14 @@ fn read_tree_recursive( + /// Intermediate structure for building nested git trees from flat paths. + struct DirNode { + files: BTreeMap, +- dirs: BTreeMap, ++ dirs: BTreeMap, + } + + impl DirNode { + fn new() -> Self { + Self { + files: BTreeMap::new(), +- dirs: BTreeMap::new(), ++ dirs: BTreeMap::new(), + } + } + } +diff --git a/lib/crates/fabro-checkpoint/src/metadata.rs b/lib/crates/fabro-checkpoint/src/metadata.rs +index a25d6f41..28c3264f 100644 +--- a/lib/crates/fabro-checkpoint/src/metadata.rs ++++ b/lib/crates/fabro-checkpoint/src/metadata.rs +@@ -15,14 +15,14 @@ use crate::git::Store; + /// (`fabro/meta/{run_id}`) so that runs can be resumed from git alone. + pub struct MetadataStore { + repo_path: PathBuf, +- author: GitAuthor, ++ author: GitAuthor, + } + + impl MetadataStore { + pub fn new(repo_path: impl Into, author: &GitAuthor) -> Self { + Self { + repo_path: repo_path.into(), +- author: author.clone(), ++ author: author.clone(), + } + } + +@@ -369,10 +369,11 @@ mod tests { + let checkpoint_json = + serde_json::to_vec_pretty(&test_checkpoint("node_a", Vec::new(), None)).unwrap(); + store +- .write_checkpoint(&run_id, &checkpoint_json, &[( +- "artifacts/response.plan.json", +- artifact_data.as_slice(), +- )]) ++ .write_checkpoint( ++ &run_id, ++ &checkpoint_json, ++ &[("artifacts/response.plan.json", artifact_data.as_slice())], ++ ) + .unwrap(); + + let read_back = MetadataStore::read_artifact(dir.path(), &run_id, "response.plan") +@@ -433,10 +434,10 @@ mod tests { + let run_id = fixtures::RUN_6.to_string(); + let store = MetadataStore::new(dir.path(), &GitAuthor::default()); + let start_record = StartRecord { +- run_id: fixtures::RUN_6, ++ run_id: fixtures::RUN_6, + start_time: Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).single().unwrap(), + run_branch: Some("fabro/run/test".to_string()), +- base_sha: None, ++ base_sha: None, + }; + let bytes = serde_json::to_vec_pretty(&start_record).unwrap(); + store.init_run(&run_id, &[("start.json", &bytes)]).unwrap(); +diff --git a/lib/crates/fabro-checkpoint/src/trailer.rs b/lib/crates/fabro-checkpoint/src/trailer.rs +index cd97085a..a94dff43 100644 +--- a/lib/crates/fabro-checkpoint/src/trailer.rs ++++ b/lib/crates/fabro-checkpoint/src/trailer.rs +@@ -2,7 +2,7 @@ use std::fmt::Write; + + /// A git commit message trailer (key-value pair). + pub struct Trailer<'a> { +- pub key: &'a str, ++ pub key: &'a str, + pub value: &'a str, + } + +@@ -92,20 +92,26 @@ mod tests { + + #[test] + fn append_to_simple_message() { +- let result = append("Initial commit", &Trailer { +- key: "My-Checkpoint", +- value: "abc123", +- }); ++ let result = append( ++ "Initial commit", ++ &Trailer { ++ key: "My-Checkpoint", ++ value: "abc123", ++ }, ++ ); + assert_eq!(result, "Initial commit\n\nMy-Checkpoint: abc123\n"); + } + + #[test] + fn append_to_message_with_existing_trailer() { + let msg = "Initial commit\n\nSigned-off-by: Alice \n"; +- let result = append(msg, &Trailer { +- key: "My-Checkpoint", +- value: "abc123", +- }); ++ let result = append( ++ msg, ++ &Trailer { ++ key: "My-Checkpoint", ++ value: "abc123", ++ }, ++ ); + assert_eq!( + result, + "Initial commit\n\nSigned-off-by: Alice \nMy-Checkpoint: abc123\n" +@@ -115,10 +121,13 @@ mod tests { + #[test] + fn append_to_message_with_body_no_trailer() { + let msg = "Initial commit\n\nThis is a longer description of the change.\n"; +- let result = append(msg, &Trailer { +- key: "My-Checkpoint", +- value: "abc123", +- }); ++ let result = append( ++ msg, ++ &Trailer { ++ key: "My-Checkpoint", ++ value: "abc123", ++ }, ++ ); + assert_eq!( + result, + "Initial commit\n\nThis is a longer description of the change.\n\nMy-Checkpoint: abc123\n" +@@ -167,16 +176,20 @@ mod tests { + + #[test] + fn format_message_with_trailers() { +- let result = format_message("Initial commit", "", &[ +- Trailer { +- key: "Signed-off-by", +- value: "Alice", +- }, +- Trailer { +- key: "My-Checkpoint", +- value: "abc123", +- }, +- ]); ++ let result = format_message( ++ "Initial commit", ++ "", ++ &[ ++ Trailer { ++ key: "Signed-off-by", ++ value: "Alice", ++ }, ++ Trailer { ++ key: "My-Checkpoint", ++ value: "abc123", ++ }, ++ ], ++ ); + assert_eq!( + result, + "Initial commit\n\nSigned-off-by: Alice\nMy-Checkpoint: abc123\n" +@@ -185,10 +198,14 @@ mod tests { + + #[test] + fn format_message_with_body_and_trailers() { +- let result = format_message("Initial commit", "Description here", &[Trailer { +- key: "My-Checkpoint", +- value: "abc123", +- }]); ++ let result = format_message( ++ "Initial commit", ++ "Description here", ++ &[Trailer { ++ key: "My-Checkpoint", ++ value: "abc123", ++ }], ++ ); + assert_eq!( + result, + "Initial commit\n\nDescription here\n\nMy-Checkpoint: abc123\n" +diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs +index bd7a3160..fe2d2058 100644 +--- a/lib/crates/fabro-cli/src/args.rs ++++ b/lib/crates/fabro-cli/src/args.rs +@@ -297,17 +297,17 @@ pub(crate) struct LogsArgs { + pub(crate) server: ServerTargetArgs, + + /// Run ID prefix or workflow name (most recent run) +- pub(crate) run: String, ++ pub(crate) run: String, + /// Follow log output + #[arg(short, long)] + pub(crate) follow: bool, + /// Logs since timestamp or relative (e.g. "42m", "2h", + /// "2026-01-02T13:00:00Z") + #[arg(long)] +- pub(crate) since: Option, ++ pub(crate) since: Option, + /// Lines from end (default: all) + #[arg(short = 'n', long)] +- pub(crate) tail: Option, ++ pub(crate) tail: Option, + /// Formatted colored output with rendered assistant text + #[arg(short = 'p', long)] + pub(crate) pretty: bool, +@@ -428,9 +428,9 @@ pub(crate) struct CpArgs { + pub(crate) server: ServerTargetArgs, + + /// Source: : or local path +- pub(crate) src: String, ++ pub(crate) src: String, + /// Destination: : or local path +- pub(crate) dst: String, ++ pub(crate) dst: String, + /// Recurse into directories + #[arg(short, long)] + pub(crate) recursive: bool, +@@ -442,18 +442,18 @@ pub(crate) struct PreviewArgs { + pub(crate) server: ServerTargetArgs, + + /// Run ID or prefix +- pub(crate) run: String, ++ pub(crate) run: String, + /// Port number +- pub(crate) port: u16, ++ pub(crate) port: u16, + /// Generate a signed URL (embeds auth token, no headers needed) + #[arg(long)] + pub(crate) signed: bool, + /// Signed URL expiry in seconds (default 3600, requires --signed) + #[arg(long, default_value = "3600", requires = "signed")] +- pub(crate) ttl: i32, ++ pub(crate) ttl: i32, + /// Open URL in browser (implies --signed) + #[arg(long)] +- pub(crate) open: bool, ++ pub(crate) open: bool, + } + + #[derive(Args)] +@@ -462,10 +462,10 @@ pub(crate) struct SshArgs { + pub(crate) server: ServerTargetArgs, + + /// Run ID or prefix +- pub(crate) run: String, ++ pub(crate) run: String, + /// SSH access expiry in minutes (default 60) + #[arg(long, default_value = "60")] +- pub(crate) ttl: f64, ++ pub(crate) ttl: f64, + /// Print the SSH command instead of connecting + #[arg(long)] + pub(crate) print: bool, +@@ -477,7 +477,7 @@ pub(crate) struct DiffArgs { + pub(crate) server: ServerTargetArgs, + + /// Run ID or prefix +- pub(crate) run: String, ++ pub(crate) run: String, + /// Show diff for a specific node + #[arg(long)] + pub(crate) node: Option, +@@ -523,14 +523,14 @@ pub(crate) enum SecretTypeArg { + #[derive(Args)] + pub(crate) struct SecretSetArgs { + /// Name of the secret +- pub(crate) key: String, ++ pub(crate) key: String, + /// Value to store (omit to enter interactively) +- pub(crate) value: Option, ++ pub(crate) value: Option, + /// Read the secret value from stdin + #[arg(long, conflicts_with = "value")] + pub(crate) value_stdin: bool, + #[arg(long, value_enum, default_value = "environment")] +- pub(crate) r#type: SecretTypeArg, ++ pub(crate) r#type: SecretTypeArg, + #[arg(long)] + pub(crate) description: Option, + } +@@ -709,10 +709,10 @@ pub(crate) struct PrCreateArgs { + pub(crate) run_id: String, + /// LLM model for generating PR description + #[arg(long)] +- pub(crate) model: Option, ++ pub(crate) model: Option, + /// Create PR even if the run status is not success/partial_success + #[arg(short, long)] +- pub(crate) force: bool, ++ pub(crate) force: bool, + } + + #[derive(Args)] +@@ -994,7 +994,7 @@ pub(crate) enum Commands { + /// Set up the Fabro environment (LLMs, certs, GitHub) + Install { + #[command(flatten)] +- args: InstallArgs, ++ args: InstallArgs, + #[command(subcommand)] + command: Option, + }, +diff --git a/lib/crates/fabro-cli/src/command_context.rs b/lib/crates/fabro-cli/src/command_context.rs +index 2361e8f3..151bebe8 100644 +--- a/lib/crates/fabro-cli/src/command_context.rs ++++ b/lib/crates/fabro-cli/src/command_context.rs +@@ -19,20 +19,20 @@ pub(crate) enum ServerMode { + target_override: Option, + }, + ByStorageDir { +- target_override: Option, ++ target_override: Option, + storage_dir_override: Option, + }, + } + + pub(crate) struct CommandContext { + #[allow(dead_code)] +- printer: Printer, +- cwd: PathBuf, ++ printer: Printer, ++ cwd: PathBuf, + base_config_path: PathBuf, + machine_settings: SettingsLayer, +- cli_settings: CliSettings, +- server_mode: ServerMode, +- server: OnceCell>, ++ cli_settings: CliSettings, ++ server_mode: ServerMode, ++ server: OnceCell>, + } + + impl CommandContext { +@@ -69,7 +69,7 @@ impl CommandContext { + Self::new( + printer, + ServerMode::ByStorageDir { +- target_override: args.target.server.clone(), ++ target_override: args.target.server.clone(), + storage_dir_override: args.storage_dir.clone_path(), + }, + cli_settings, +@@ -92,10 +92,13 @@ impl CommandContext { + .. + } => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?, + }; +- let machine_settings = combine_files(disk_settings, SettingsLayer { +- cli: Some(cli_layer.clone()), +- ..SettingsLayer::default() +- }); ++ let machine_settings = combine_files( ++ disk_settings, ++ SettingsLayer { ++ cli: Some(cli_layer.clone()), ++ ..SettingsLayer::default() ++ }, ++ ); + + Ok(Self { + printer, +diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs +index dba10bd5..9d2f5be2 100644 +--- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs ++++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs +@@ -197,11 +197,11 @@ mod tests { + #[test] + fn format_candidate_includes_retry() { + let entry = super::super::ArtifactEntry { +- node_slug: "retry_assets".to_string(), +- retry: 2, +- stage_id: fabro_types::StageId::new("retry_assets", 2), ++ node_slug: "retry_assets".to_string(), ++ retry: 2, ++ stage_id: fabro_types::StageId::new("retry_assets", 2), + relative_path: "assets/retry/report.txt".to_string(), +- size: 6, ++ size: 6, + }; + + assert_eq!(format_candidate(&entry), "retry_assets:retry_2"); +diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs +index 930dc422..cb2893c6 100644 +--- a/lib/crates/fabro-cli/src/commands/artifact/mod.rs ++++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs +@@ -15,11 +15,11 @@ use crate::server_runs::ServerSummaryLookup; + #[derive(Clone, Debug, serde::Serialize)] + pub(super) struct ArtifactEntry { + #[serde(skip_serializing)] +- pub(super) stage_id: StageId, +- pub(super) node_slug: String, +- pub(super) retry: u32, ++ pub(super) stage_id: StageId, ++ pub(super) node_slug: String, ++ pub(super) retry: u32, + pub(super) relative_path: String, +- pub(super) size: u64, ++ pub(super) size: u64, + } + + pub(super) async fn resolve_artifacts( +diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs +index 894dc4da..ae7f1836 100644 +--- a/lib/crates/fabro-cli/src/commands/doctor.rs ++++ b/lib/crates/fabro-cli/src/commands/doctor.rs +@@ -30,10 +30,10 @@ pub(crate) fn check_config( + (Some(path), true) => { + let display = contract_tilde(&path); + CheckResult { +- name: "Configuration".to_string(), +- status: CheckStatus::Pass, +- summary: display.display().to_string(), +- details: vec![CheckDetail::new(format!( ++ name: "Configuration".to_string(), ++ status: CheckStatus::Pass, ++ summary: display.display().to_string(), ++ details: vec![CheckDetail::new(format!( + "Loaded from {}", + display.display() + ))], +@@ -43,10 +43,10 @@ pub(crate) fn check_config( + (Some(path), false) => { + let display = contract_tilde(&path); + CheckResult { +- name: "Configuration".to_string(), +- status: CheckStatus::Warning, +- summary: display.display().to_string(), +- details: std::iter::once(CheckDetail::new(format!( ++ name: "Configuration".to_string(), ++ status: CheckStatus::Warning, ++ summary: display.display().to_string(), ++ details: std::iter::once(CheckDetail::new(format!( + "Loaded from {}", + display.display() + ))) +@@ -62,10 +62,10 @@ pub(crate) fn check_config( + } + } + (None, false) => CheckResult { +- name: "Configuration".to_string(), +- status: CheckStatus::Warning, +- summary: "legacy config files ignored".to_string(), +- details: legacy_paths ++ name: "Configuration".to_string(), ++ status: CheckStatus::Warning, ++ summary: "legacy config files ignored".to_string(), ++ details: legacy_paths + .iter() + .map(|legacy| { + CheckDetail::new(format!("Found legacy config file {}", legacy.display())) +@@ -78,10 +78,10 @@ pub(crate) fn check_config( + remediation: Some("Create ~/.fabro/settings.toml".to_string()), + }, + (None, true) => CheckResult { +- name: "Configuration".to_string(), +- status: CheckStatus::Warning, +- summary: "no settings config file found".to_string(), +- details: vec![CheckDetail::new( ++ name: "Configuration".to_string(), ++ status: CheckStatus::Warning, ++ summary: "no settings config file found".to_string(), ++ details: vec![CheckDetail::new( + "Create ~/.fabro/settings.toml to configure Fabro".to_string(), + )], + remediation: Some("Create ~/.fabro/settings.toml".to_string()), +@@ -93,10 +93,10 @@ fn check_legacy_env(path: Option) -> Option { + path.map(|path| { + let display = contract_tilde(&path); + CheckResult { +- name: "Legacy .env".to_string(), +- status: CheckStatus::Warning, +- summary: "legacy secrets file detected".to_string(), +- details: vec![CheckDetail::new(format!( ++ name: "Legacy .env".to_string(), ++ status: CheckStatus::Warning, ++ summary: "legacy secrets file detected".to_string(), ++ details: vec![CheckDetail::new(format!( + "{} is no longer read by fabro", + display.display() + ))], +@@ -107,8 +107,8 @@ fn check_legacy_env(path: Option) -> Option { + + #[derive(Debug, Clone, PartialEq, Eq)] + struct StorageDirStatus { +- path: PathBuf, +- exists: bool, ++ path: PathBuf, ++ exists: bool, + readable: bool, + writable: bool, + } +@@ -168,20 +168,20 @@ fn check_version_parity(server_version: &str) -> CheckResult { + let cli_version = FABRO_VERSION; + if server_version == cli_version { + CheckResult { +- name: "Version parity".to_string(), +- status: CheckStatus::Pass, +- summary: cli_version.to_string(), +- details: vec![CheckDetail::new(format!( ++ name: "Version parity".to_string(), ++ status: CheckStatus::Pass, ++ summary: cli_version.to_string(), ++ details: vec![CheckDetail::new(format!( + "CLI and server are both {cli_version}" + ))], + remediation: None, + } + } else { + CheckResult { +- name: "Version parity".to_string(), +- status: CheckStatus::Warning, +- summary: format!("CLI {cli_version}, server {server_version}"), +- details: vec![CheckDetail::new(format!( ++ name: "Version parity".to_string(), ++ status: CheckStatus::Warning, ++ summary: format!("CLI {cli_version}, server {server_version}"), ++ details: vec![CheckDetail::new(format!( + "CLI version {cli_version} does not match server version {server_version}" + ))], + remediation: Some( +@@ -194,10 +194,10 @@ fn check_version_parity(server_version: &str) -> CheckResult { + + fn skipped_version_parity(reason: &str) -> CheckResult { + CheckResult { +- name: "Version parity".to_string(), +- status: CheckStatus::Warning, +- summary: "skipped".to_string(), +- details: vec![CheckDetail::new(format!( ++ name: "Version parity".to_string(), ++ status: CheckStatus::Warning, ++ summary: "skipped".to_string(), ++ details: vec![CheckDetail::new(format!( + "Could not retrieve server version: {reason}" + ))], + remediation: None, +@@ -216,15 +216,15 @@ fn convert_diagnostics_sections(sections: Vec) -> + sections + .into_iter() + .map(|section| CheckSection { +- title: section.title, ++ title: section.title, + checks: section + .checks + .into_iter() + .map(|check| CheckResult { +- name: check.name, +- status: convert_diagnostics_status(check.status), +- summary: check.summary, +- details: check ++ name: check.name, ++ status: convert_diagnostics_status(check.status), ++ summary: check.summary, ++ details: check + .details + .into_iter() + .map(|detail| CheckDetail { +@@ -317,9 +317,9 @@ pub(crate) async fn run_doctor( + } + + let mut report = CheckReport { +- title: "Fabro Doctor".to_string(), ++ title: "Fabro Doctor".to_string(), + sections: vec![CheckSection { +- title: "Local".to_string(), ++ title: "Local".to_string(), + checks: local_checks, + }], + }; +@@ -328,12 +328,12 @@ pub(crate) async fn run_doctor( + Ok(ctx) => ctx, + Err(err) => { + report.sections.push(CheckSection { +- title: "Server".to_string(), ++ title: "Server".to_string(), + checks: vec![CheckResult { +- name: "Fabro server".to_string(), +- status: CheckStatus::Error, +- summary: "settings resolution failed".to_string(), +- details: vec![CheckDetail::new(err.to_string())], ++ name: "Fabro server".to_string(), ++ status: CheckStatus::Error, ++ summary: "settings resolution failed".to_string(), ++ details: vec![CheckDetail::new(err.to_string())], + remediation: Some( + "Fix the local CLI settings or provide `--server`, then run doctor again." + .to_string(), +@@ -358,12 +358,12 @@ pub(crate) async fn run_doctor( + Ok(server) => server, + Err(err) => { + report.sections.push(CheckSection { +- title: "Server".to_string(), ++ title: "Server".to_string(), + checks: vec![CheckResult { +- name: "Fabro server".to_string(), +- status: CheckStatus::Error, +- summary: "unreachable".to_string(), +- details: vec![CheckDetail::new(err.to_string())], ++ name: "Fabro server".to_string(), ++ status: CheckStatus::Error, ++ summary: "unreachable".to_string(), ++ details: vec![CheckDetail::new(err.to_string())], + remediation: Some( + "Start or connect to the server with `--server` and run doctor again." + .to_string(), +@@ -386,12 +386,12 @@ pub(crate) async fn run_doctor( + + if let Err(err) = server.api().get_health().send().await { + report.sections.push(CheckSection { +- title: "Server".to_string(), ++ title: "Server".to_string(), + checks: vec![CheckResult { +- name: "Fabro server".to_string(), +- status: CheckStatus::Error, +- summary: "health check failed".to_string(), +- details: vec![CheckDetail::new(err.to_string())], ++ name: "Fabro server".to_string(), ++ status: CheckStatus::Error, ++ summary: "health check failed".to_string(), ++ details: vec![CheckDetail::new(err.to_string())], + remediation: Some( + "Check that the server is reachable and responding to /health.".to_string(), + ), +@@ -411,12 +411,12 @@ pub(crate) async fn run_doctor( + } + + report.sections.push(CheckSection { +- title: "Server".to_string(), ++ title: "Server".to_string(), + checks: vec![CheckResult { +- name: "Location".to_string(), +- status: CheckStatus::Pass, +- summary: server.base_url().to_string(), +- details: vec![], ++ name: "Location".to_string(), ++ status: CheckStatus::Pass, ++ summary: server.base_url().to_string(), ++ details: vec![], + remediation: None, + }], + }); +@@ -436,12 +436,12 @@ pub(crate) async fn run_doctor( + .checks + .push(skipped_version_parity(&err.to_string())); + report.sections.push(CheckSection { +- title: "Server".to_string(), ++ title: "Server".to_string(), + checks: vec![CheckResult { +- name: "Diagnostics".to_string(), +- status: CheckStatus::Error, +- summary: "probe failed".to_string(), +- details: vec![CheckDetail::new(err.to_string())], ++ name: "Diagnostics".to_string(), ++ status: CheckStatus::Error, ++ summary: "probe failed".to_string(), ++ details: vec![CheckDetail::new(err.to_string())], + remediation: Some( + "Fix the server diagnostics failure and run `fabro doctor` again." + .to_string(), +@@ -516,12 +516,15 @@ mod tests { + let dir = tempfile::tempdir().unwrap(); + let status = probe_storage_dir(dir.path()); + +- assert_eq!(status, StorageDirStatus { +- path: dir.path().to_path_buf(), +- exists: true, +- readable: true, +- writable: true, +- }); ++ assert_eq!( ++ status, ++ StorageDirStatus { ++ path: dir.path().to_path_buf(), ++ exists: true, ++ readable: true, ++ writable: true, ++ } ++ ); + } + + #[test] +@@ -530,19 +533,22 @@ mod tests { + let path = dir.path().join("missing"); + let status = probe_storage_dir(&path); + +- assert_eq!(status, StorageDirStatus { +- path, +- exists: false, +- readable: false, +- writable: false, +- }); ++ assert_eq!( ++ status, ++ StorageDirStatus { ++ path, ++ exists: false, ++ readable: false, ++ writable: false, ++ } ++ ); + } + + #[test] + fn check_storage_dir_pass() { + let result = check_storage_dir(&StorageDirStatus { +- path: PathBuf::from("/home/user/.fabro"), +- exists: true, ++ path: PathBuf::from("/home/user/.fabro"), ++ exists: true, + readable: true, + writable: true, + }); +@@ -556,8 +562,8 @@ mod tests { + #[test] + fn check_storage_dir_not_exists() { + let result = check_storage_dir(&StorageDirStatus { +- path: PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz"), +- exists: false, ++ path: PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz"), ++ exists: false, + readable: false, + writable: false, + }); +@@ -570,8 +576,8 @@ mod tests { + #[test] + fn check_storage_dir_not_writable() { + let result = check_storage_dir(&StorageDirStatus { +- path: PathBuf::from("/home/user/.fabro"), +- exists: true, ++ path: PathBuf::from("/home/user/.fabro"), ++ exists: true, + readable: true, + writable: false, + }); +@@ -605,14 +611,14 @@ mod tests { + #[test] + fn render_report_text_without_color_has_no_ansi() { + let report = CheckReport { +- title: "Fabro Doctor".to_string(), ++ title: "Fabro Doctor".to_string(), + sections: vec![CheckSection { +- title: "Local".to_string(), ++ title: "Local".to_string(), + checks: vec![CheckResult { +- name: "Configuration".to_string(), +- status: CheckStatus::Pass, +- summary: "loaded".to_string(), +- details: vec![CheckDetail::new( ++ name: "Configuration".to_string(), ++ status: CheckStatus::Pass, ++ summary: "loaded".to_string(), ++ details: vec![CheckDetail::new( + "Loaded from ~/.fabro/settings.toml".into(), + )], + remediation: None, +diff --git a/lib/crates/fabro-cli/src/commands/exec.rs b/lib/crates/fabro-cli/src/commands/exec.rs +index 28fe4b69..cc6c2519 100644 +--- a/lib/crates/fabro-cli/src/commands/exec.rs ++++ b/lib/crates/fabro-cli/src/commands/exec.rs +@@ -39,7 +39,7 @@ fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings { + } + } + McpEntryLayer::Http { url, headers, .. } => McpTransport::Http { +- url: url.as_source(), ++ url: url.as_source(), + headers: headers + .iter() + .map(|(key, value)| (key.clone(), value.as_source())) +@@ -141,10 +141,10 @@ pub(crate) async fn execute( + .mcps + .values() + .map(|server| McpServerSettings { +- name: server.name.clone(), +- transport: server.transport.clone(), ++ name: server.name.clone(), ++ transport: server.transport.clone(), + startup_timeout_secs: server.startup_timeout_secs, +- tool_timeout_secs: server.tool_timeout_secs, ++ tool_timeout_secs: server.tool_timeout_secs, + }) + .collect() + } else if let Some(mcps) = raw_settings +@@ -166,10 +166,10 @@ pub(crate) async fn execute( + .mcps + .values() + .map(|server| McpServerSettings { +- name: server.name.clone(), +- transport: server.transport.clone(), ++ name: server.name.clone(), ++ transport: server.transport.clone(), + startup_timeout_secs: server.startup_timeout_secs, +- tool_timeout_secs: server.tool_timeout_secs, ++ tool_timeout_secs: server.tool_timeout_secs, + }) + .collect() + }) +diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs +index 12a873dc..2b47c04b 100644 +--- a/lib/crates/fabro-cli/src/commands/graph.rs ++++ b/lib/crates/fabro-cli/src/commands/graph.rs +@@ -30,12 +30,12 @@ pub(crate) async fn run( + + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let built = build_run_manifest(ManifestBuildInput { +- workflow: args.workflow.clone(), +- cwd: ctx.cwd().to_path_buf(), +- args_layer: SettingsLayer::default(), +- args: None, +- run_id: None, +- user_layer: load_settings_user()?, ++ workflow: args.workflow.clone(), ++ cwd: ctx.cwd().to_path_buf(), ++ args_layer: SettingsLayer::default(), ++ args: None, ++ run_id: None, ++ user_layer: load_settings_user()?, + user_settings_path: Some(active_settings_path(None)), + })?; + let client = ctx.server().await?; +@@ -52,8 +52,8 @@ pub(crate) async fn run( + + let rendered = client + .render_workflow_graph(types::RenderWorkflowGraphRequest { +- manifest: built.manifest, +- format: Some(types::RenderWorkflowGraphFormat::Svg), ++ manifest: built.manifest, ++ format: Some(types::RenderWorkflowGraphFormat::Svg), + direction: args.direction.map(|direction| match direction { + GraphDirection::Lr => types::RenderWorkflowGraphDirection::Lr, + GraphDirection::Tb => types::RenderWorkflowGraphDirection::Tb, +diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs +index ae35373c..753d644c 100644 +--- a/lib/crates/fabro-cli/src/commands/install.rs ++++ b/lib/crates/fabro-cli/src/commands/install.rs +@@ -21,6 +21,7 @@ use fabro_config::{ResolveError, Storage, envfile, legacy_env}; + use fabro_model::Provider; + use fabro_server::bind::Bind; + use fabro_server::serve; ++use fabro_server::serve::DEFAULT_TCP_PORT; + use fabro_store::ArtifactStore; + use fabro_types::settings::cli::{CliLayer, OutputFormat}; + use fabro_types::settings::server::ServerAuthMethod; +@@ -158,7 +159,7 @@ fn ensure_table<'a>(table: &'a mut toml::Table, key: &str) -> Result<&'a mut tom + + /// Default web URL used by `fabro install` when `--web-url` is omitted. + pub(crate) fn default_web_url() -> String { +- format!("http://127.0.0.1:{}", fabro_server::serve::DEFAULT_TCP_PORT) ++ format!("http://127.0.0.1:{DEFAULT_TCP_PORT}") + } + + fn merge_server_settings(doc: &mut toml::Value, web_url: &str) -> Result<()> { +@@ -439,7 +440,7 @@ enum GitHubInstallSelection { + token: String, + }, + App { +- owner: GitHubAppOwner, ++ owner: GitHubAppOwner, + username: Option, + }, + } +@@ -791,11 +792,9 @@ impl InstallInputSource for NonInteractiveInstallInputSource { + Ok(GitHubInstallSelection::Token { token }) + } + Some(InstallGitHubStrategyArg::App) => Ok(GitHubInstallSelection::App { +- owner: GitHubAppOwner::parse_scripted( +- self.args.github_owner.as_deref().context( +- "non-interactive install requires --github-owner for --github-strategy app", +- )?, +- )?, ++ owner: GitHubAppOwner::parse_scripted(self.args.github_owner.as_deref().context( ++ "non-interactive install requires --github-owner for --github-strategy app", ++ )?)?, + username: best_effort_github_username().await, + }), + None => bail!("non-interactive install requires --github-strategy"), +@@ -876,7 +875,7 @@ async fn choose_install_github_selection( + Ok(GitHubInstallSelection::Token { token }) + } + Some(InstallGitHubStrategyArg::App) => Ok(GitHubInstallSelection::App { +- owner: GitHubAppOwner::parse_scripted(github_args.owner.as_deref().context( ++ owner: GitHubAppOwner::parse_scripted(github_args.owner.as_deref().context( + "install github --non-interactive requires --owner for --strategy app", + )?)?, + username: best_effort_github_username().await, +@@ -1039,8 +1038,8 @@ fn build_github_app_manifest(app_name: &str, port: u16, web_url: &str) -> serde_ + /// Run the GitHub App manifest registration flow via a temporary local server. + /// Returns the app metadata and secret pairs to persist for the local server. + struct GitHubAppRegistration { +- app_id: String, +- slug: String, ++ app_id: String, ++ slug: String, + client_id: String, + env_pairs: Vec<(String, String)>, + } +@@ -1048,17 +1047,17 @@ struct GitHubAppRegistration { + enum PendingGitHubSettings { + Token, + App { +- app_id: String, +- slug: String, +- client_id: String, ++ app_id: String, ++ slug: String, ++ client_id: String, + allowed_usernames: Vec, + }, + } + + #[derive(Clone, Copy)] + struct PendingSettingsWrite<'a> { +- path: &'a Path, +- contents: &'a str, ++ path: &'a Path, ++ contents: &'a str, + previous_contents: Option<&'a str>, + } + +@@ -1274,9 +1273,9 @@ async fn persist_vault_secrets_via_server( + client + .create_secret() + .body(CreateSecretRequest { +- name: secret.name.clone(), +- value: secret.value.clone(), +- type_: secret.type_, ++ name: secret.name.clone(), ++ value: secret.value.clone(), ++ type_: secret.type_, + description: secret.description.clone(), + }) + .send() +@@ -1315,9 +1314,9 @@ async fn persist_vault_secrets_with( + + fn credential_secret_request(credential: &AuthCredential) -> Result { + Ok(CreateSecretRequest { +- name: credential_id_for(credential).map_err(anyhow::Error::msg)?, +- value: serde_json::to_string(credential)?, +- type_: ApiSecretType::Credential, ++ name: credential_id_for(credential).map_err(anyhow::Error::msg)?, ++ value: serde_json::to_string(credential)?, ++ type_: ApiSecretType::Credential, + description: None, + }) + } +@@ -1354,11 +1353,11 @@ async fn persist_install_outputs( + } + + struct PendingGitHubInstallWrite<'a> { +- settings_write: PendingSettingsWrite<'a>, +- server_env_set: Vec<(String, String)>, ++ settings_write: PendingSettingsWrite<'a>, ++ server_env_set: Vec<(String, String)>, + server_env_remove: Vec<&'static str>, +- vault_set: Vec<(String, String)>, +- vault_remove: Vec<&'static str>, ++ vault_set: Vec<(String, String)>, ++ vault_remove: Vec<&'static str>, + } + + fn restore_optional_file(path: &Path, previous_contents: Option<&str>) -> Result<()> { +@@ -1702,17 +1701,20 @@ async fn run_install_github_inner( + } + + let settings_toml = toml::to_string_pretty(&doc)?; +- persist_github_install_changes(&storage_dir, &PendingGitHubInstallWrite { +- settings_write: PendingSettingsWrite { +- path: &config_path, +- contents: settings_toml.as_str(), +- previous_contents: Some(existing_config_contents.as_str()), ++ persist_github_install_changes( ++ &storage_dir, ++ &PendingGitHubInstallWrite { ++ settings_write: PendingSettingsWrite { ++ path: &config_path, ++ contents: settings_toml.as_str(), ++ previous_contents: Some(existing_config_contents.as_str()), ++ }, ++ server_env_set, ++ server_env_remove, ++ vault_set, ++ vault_remove, + }, +- server_env_set, +- server_env_remove, +- vault_set, +- vault_remove, +- })?; ++ )?; + + if let Some(restart_outcome) = + maybe_restart_server_after_github_install(&storage_dir, &config_path, server_was_running) +@@ -1865,9 +1867,9 @@ async fn run_install_inner( + s.green.apply_to("✔") + ); + vault_secrets.push(CreateSecretRequest { +- name: "GITHUB_TOKEN".to_string(), +- value: token, +- type_: ApiSecretType::Environment, ++ name: "GITHUB_TOKEN".to_string(), ++ value: token, ++ type_: ApiSecretType::Environment, + description: None, + }); + Some(PendingGitHubSettings::Token) +@@ -1898,9 +1900,9 @@ async fn run_install_inner( + ); + server_env_pairs.extend(registration.env_pairs.iter().cloned()); + Some(PendingGitHubSettings::App { +- app_id: registration.app_id, +- slug: registration.slug, +- client_id: registration.client_id, ++ app_id: registration.app_id, ++ slug: registration.slug, ++ client_id: registration.client_id, + allowed_usernames: vec![allowed_username], + }) + } +@@ -2005,8 +2007,8 @@ async fn run_install_inner( + &server_env_pairs, + &vault_secrets, + Some(PendingSettingsWrite { +- path: &config_path, +- contents: settings_toml.as_str(), ++ path: &config_path, ++ contents: settings_toml.as_str(), + previous_contents: existing_config_contents.as_deref(), + }), + server_was_running, +@@ -2088,7 +2090,7 @@ async fn run_install_inner( + || async { + fabro_util::printerr!(printer, ""); + let doctor_args = DoctorArgs { +- target: ServerTargetArgs::default(), ++ target: ServerTargetArgs::default(), + verbose: false, + }; + doctor::run_doctor(&doctor_args, false, cli, cli_layer, printer).await +@@ -2220,9 +2222,10 @@ mod tests { + .and_then(|s| s.auth.as_ref()) + .and_then(|a| a.methods.clone()) + .expect("server.auth.methods should be set"); +- assert_eq!(methods, vec![ +- fabro_types::settings::ServerAuthMethod::DevToken +- ]); ++ assert_eq!( ++ methods, ++ vec![fabro_types::settings::ServerAuthMethod::DevToken] ++ ); + } + + #[test] +@@ -2421,9 +2424,13 @@ client_id = "client-id" + let mut doc = toml::Value::Table(toml::Table::default()); + merge_server_settings(&mut doc, &default_web_url()).unwrap(); + +- write_github_app_settings(&mut doc, "123", "fabro-app", "client-id", &[ +- "brynary".to_string() +- ]) ++ write_github_app_settings( ++ &mut doc, ++ "123", ++ "fabro-app", ++ "client-id", ++ &["brynary".to_string()], ++ ) + .unwrap(); + + let github = doc +@@ -2611,14 +2618,14 @@ client_id = "client-id" + ]; + let vault_secrets = vec![ + CreateSecretRequest { +- name: "GITHUB_TOKEN".to_string(), +- value: "gh-token".to_string(), +- type_: ApiSecretType::Environment, ++ name: "GITHUB_TOKEN".to_string(), ++ value: "gh-token".to_string(), ++ type_: ApiSecretType::Environment, + description: None, + }, + credential_secret_request(&AuthCredential { + provider: Provider::Anthropic, +- details: fabro_auth::AuthDetails::ApiKey { ++ details: fabro_auth::AuthDetails::ApiKey { + key: "anthropic-key".to_string(), + }, + }) +@@ -2682,9 +2689,9 @@ client_id = "client-id" + async fn persist_vault_secrets_with_leaves_running_server_up() { + let dir = tempfile::tempdir().unwrap(); + let vault_secrets = vec![CreateSecretRequest { +- name: "GITHUB_TOKEN".to_string(), +- value: "gh-token".to_string(), +- type_: ApiSecretType::Environment, ++ name: "GITHUB_TOKEN".to_string(), ++ value: "gh-token".to_string(), ++ type_: ApiSecretType::Environment, + description: None, + }]; + let server = MockServer::start_async().await; +@@ -2896,9 +2903,9 @@ client_id = "client-id" + let dir = tempfile::tempdir().unwrap(); + let server_env_pairs = vec![("SESSION_SECRET".to_string(), "session".to_string())]; + let vault_secrets = vec![CreateSecretRequest { +- name: "GITHUB_CLI_TOKEN".to_string(), +- value: "gh-token".to_string(), +- type_: ApiSecretType::Environment, ++ name: "GITHUB_CLI_TOKEN".to_string(), ++ value: "gh-token".to_string(), ++ type_: ApiSecretType::Environment, + description: None, + }]; + let settings_path = dir.path().join(SETTINGS_CONFIG_FILENAME); +@@ -2909,8 +2916,8 @@ client_id = "client-id" + &server_env_pairs, + &vault_secrets, + Some(PendingSettingsWrite { +- path: &settings_path, +- contents: "_version = 1\n", ++ path: &settings_path, ++ contents: "_version = 1\n", + previous_contents: None, + }), + false, +@@ -2939,9 +2946,9 @@ client_id = "client-id" + let dir = tempfile::tempdir().unwrap(); + let server_env_pairs = vec![("SESSION_SECRET".to_string(), "session".to_string())]; + let vault_secrets = vec![CreateSecretRequest { +- name: "GITHUB_CLI_TOKEN".to_string(), +- value: "gh-token".to_string(), +- type_: ApiSecretType::Environment, ++ name: "GITHUB_CLI_TOKEN".to_string(), ++ value: "gh-token".to_string(), ++ type_: ApiSecretType::Environment, + description: None, + }]; + let settings_path = dir.path().join(SETTINGS_CONFIG_FILENAME); +@@ -2952,8 +2959,8 @@ client_id = "client-id" + &server_env_pairs, + &vault_secrets, + Some(PendingSettingsWrite { +- path: &settings_path, +- contents: "_version = 1\n[server]\nfoo = \"bar\"\n", ++ path: &settings_path, ++ contents: "_version = 1\n[server]\nfoo = \"bar\"\n", + previous_contents: Some("_version = 1\n[server]\n"), + }), + false, +@@ -2997,21 +3004,24 @@ client_id = "client-id" + let settings_path = dir.path().join(SETTINGS_CONFIG_FILENAME); + std::fs::write(&settings_path, "before").unwrap(); + +- persist_github_install_changes(dir.path(), &PendingGitHubInstallWrite { +- settings_write: PendingSettingsWrite { +- path: &settings_path, +- contents: "after", +- previous_contents: Some("before"), ++ persist_github_install_changes( ++ dir.path(), ++ &PendingGitHubInstallWrite { ++ settings_write: PendingSettingsWrite { ++ path: &settings_path, ++ contents: "after", ++ previous_contents: Some("before"), ++ }, ++ server_env_set: Vec::new(), ++ server_env_remove: vec![ ++ GITHUB_APP_PRIVATE_KEY_KEY, ++ GITHUB_APP_CLIENT_SECRET_KEY, ++ GITHUB_APP_WEBHOOK_SECRET_KEY, ++ ], ++ vault_set: vec![(GITHUB_TOKEN_SECRET_KEY.to_string(), "token".to_string())], ++ vault_remove: Vec::new(), + }, +- server_env_set: Vec::new(), +- server_env_remove: vec![ +- GITHUB_APP_PRIVATE_KEY_KEY, +- GITHUB_APP_CLIENT_SECRET_KEY, +- GITHUB_APP_WEBHOOK_SECRET_KEY, +- ], +- vault_set: vec![(GITHUB_TOKEN_SECRET_KEY.to_string(), "token".to_string())], +- vault_remove: Vec::new(), +- }) ++ ) + .unwrap(); + + let server_env = envfile::read_env_file(&server_env_path).unwrap(); +@@ -3055,30 +3065,33 @@ client_id = "client-id" + let settings_path = dir.path().join(SETTINGS_CONFIG_FILENAME); + std::fs::write(&settings_path, "before").unwrap(); + +- persist_github_install_changes(dir.path(), &PendingGitHubInstallWrite { +- settings_write: PendingSettingsWrite { +- path: &settings_path, +- contents: "after", +- previous_contents: Some("before"), ++ persist_github_install_changes( ++ dir.path(), ++ &PendingGitHubInstallWrite { ++ settings_write: PendingSettingsWrite { ++ path: &settings_path, ++ contents: "after", ++ previous_contents: Some("before"), ++ }, ++ server_env_set: vec![ ++ ( ++ GITHUB_APP_PRIVATE_KEY_KEY.to_string(), ++ "private".to_string(), ++ ), ++ ( ++ GITHUB_APP_CLIENT_SECRET_KEY.to_string(), ++ "client".to_string(), ++ ), ++ ], ++ server_env_remove: vec![ ++ GITHUB_APP_PRIVATE_KEY_KEY, ++ GITHUB_APP_CLIENT_SECRET_KEY, ++ GITHUB_APP_WEBHOOK_SECRET_KEY, ++ ], ++ vault_set: Vec::new(), ++ vault_remove: vec![GITHUB_TOKEN_SECRET_KEY], + }, +- server_env_set: vec![ +- ( +- GITHUB_APP_PRIVATE_KEY_KEY.to_string(), +- "private".to_string(), +- ), +- ( +- GITHUB_APP_CLIENT_SECRET_KEY.to_string(), +- "client".to_string(), +- ), +- ], +- server_env_remove: vec![ +- GITHUB_APP_PRIVATE_KEY_KEY, +- GITHUB_APP_CLIENT_SECRET_KEY, +- GITHUB_APP_WEBHOOK_SECRET_KEY, +- ], +- vault_set: Vec::new(), +- vault_remove: vec![GITHUB_TOKEN_SECRET_KEY], +- }) ++ ) + .unwrap(); + + let server_env = envfile::read_env_file(&server_env_path).unwrap(); +@@ -3146,10 +3159,13 @@ root = "{}" + + #[test] + fn non_interactive_source_rejects_hidden_args_without_switch() { +- let args = install_args(false, InstallNonInteractiveArgs { +- llm_provider: Some(Provider::Anthropic), +- ..InstallNonInteractiveArgs::default() +- }); ++ let args = install_args( ++ false, ++ InstallNonInteractiveArgs { ++ llm_provider: Some(Provider::Anthropic), ++ ..InstallNonInteractiveArgs::default() ++ }, ++ ); + let err = NonInteractiveInstallInputSource::new(&args).unwrap_err(); + assert!( + err.to_string() +@@ -3159,14 +3175,17 @@ root = "{}" + + #[test] + fn non_interactive_source_rejects_conflicting_api_key_inputs() { +- let args = install_args(true, InstallNonInteractiveArgs { +- llm_provider: Some(Provider::Anthropic), +- llm_api_key_stdin: true, +- llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), +- github_strategy: Some(InstallGitHubStrategyArg::Token), +- github_username: Some("brynary".to_string()), +- ..InstallNonInteractiveArgs::default() +- }); ++ let args = install_args( ++ true, ++ InstallNonInteractiveArgs { ++ llm_provider: Some(Provider::Anthropic), ++ llm_api_key_stdin: true, ++ llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), ++ github_strategy: Some(InstallGitHubStrategyArg::Token), ++ github_username: Some("brynary".to_string()), ++ ..InstallNonInteractiveArgs::default() ++ }, ++ ); + let err = NonInteractiveInstallInputSource::new(&args).unwrap_err(); + assert!( + err.to_string() +@@ -3339,7 +3358,7 @@ root = "{}" + let err = validate_install_github_non_interactive( + &InstallGithubArgs { + strategy: Some(InstallGitHubStrategyArg::Token), +- owner: Some("personal".to_string()), ++ owner: Some("personal".to_string()), + }, + true, + ) +@@ -3356,7 +3375,7 @@ root = "{}" + let err = validate_install_github_non_interactive( + &InstallGithubArgs { + strategy: Some(InstallGitHubStrategyArg::App), +- owner: None, ++ owner: None, + }, + true, + ) +diff --git a/lib/crates/fabro-cli/src/commands/model.rs b/lib/crates/fabro-cli/src/commands/model.rs +index 68c23407..c48dc355 100644 +--- a/lib/crates/fabro-cli/src/commands/model.rs ++++ b/lib/crates/fabro-cli/src/commands/model.rs +@@ -24,21 +24,21 @@ enum ModelTestResultKind { + + #[derive(Serialize)] + struct ModelTestRow { +- model: String, ++ model: String, + provider: Provider, +- result: ModelTestResultKind, ++ result: ModelTestResultKind, + #[serde(skip_serializing_if = "Option::is_none")] +- detail: Option, ++ detail: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- error: Option, ++ error: Option, + } + + #[derive(Serialize)] + struct ModelTestOutput { +- results: Vec, +- total: usize, ++ results: Vec, ++ total: usize, + failures: u32, +- skipped: u32, ++ skipped: u32, + } + + pub(crate) async fn execute( +@@ -159,25 +159,25 @@ fn model_test_row_from_status(model: &Model, status: &str, result_color: Color) + let trimmed = status.trim(); + match result_color { + Color::Green => ModelTestRow { +- model: model.id.clone(), ++ model: model.id.clone(), + provider: model.provider, +- result: ModelTestResultKind::Pass, +- detail: None, +- error: None, ++ result: ModelTestResultKind::Pass, ++ detail: None, ++ error: None, + }, + Color::Yellow => ModelTestRow { +- model: model.id.clone(), ++ model: model.id.clone(), + provider: model.provider, +- result: ModelTestResultKind::Skip, +- detail: Some(trimmed.to_string()), +- error: None, ++ result: ModelTestResultKind::Skip, ++ detail: Some(trimmed.to_string()), ++ error: None, + }, + _ => ModelTestRow { +- model: model.id.clone(), ++ model: model.id.clone(), + provider: model.provider, +- result: ModelTestResultKind::Fail, +- detail: None, +- error: Some( ++ result: ModelTestResultKind::Fail, ++ detail: None, ++ error: Some( + trimmed + .strip_prefix("error: ") + .unwrap_or(trimmed) +@@ -477,19 +477,19 @@ mod tests { + display_name: format!("{id} display"), + limits: ModelLimits { + context_window: 128_000, +- max_output: Some(4096), ++ max_output: Some(4096), + }, + training: None, + knowledge_cutoff: None, + features: ModelFeatures { +- tools: true, +- vision: false, ++ tools: true, ++ vision: false, + reasoning: false, +- effort: false, ++ effort: false, + }, + costs: ModelCosts { +- input_cost_per_mtok: Some(1.0), +- output_cost_per_mtok: Some(2.0), ++ input_cost_per_mtok: Some(1.0), ++ output_cost_per_mtok: Some(2.0), + cache_input_cost_per_mtok: None, + }, + estimated_output_tps: Some(100.0), +diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs +index bcd497a1..6c3762ca 100644 +--- a/lib/crates/fabro-cli/src/commands/pr/list.rs ++++ b/lib/crates/fabro-cli/src/commands/pr/list.rs +@@ -18,9 +18,9 @@ use crate::shared::{color_if, print_json_pretty}; + struct PrRow { + run_id: String, + number: u64, +- state: String, +- title: String, +- url: String, ++ state: String, ++ title: String, ++ url: String, + } + + pub(super) async fn list_command( +diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs +index 6ebba616..7caf4482 100644 +--- a/lib/crates/fabro-cli/src/commands/preflight.rs ++++ b/lib/crates/fabro-cli/src/commands/preflight.rs +@@ -26,12 +26,12 @@ pub(crate) async fn execute( + args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose; + + let manifest = build_run_manifest(ManifestBuildInput { +- workflow: args.workflow.clone(), +- cwd: ctx.cwd().to_path_buf(), +- args_layer: preflight_args_layer(&args)?, +- args: preflight_manifest_args(&args), +- run_id: None, +- user_layer: load_settings_user()?, ++ workflow: args.workflow.clone(), ++ cwd: ctx.cwd().to_path_buf(), ++ args_layer: preflight_args_layer(&args)?, ++ args: preflight_manifest_args(&args), ++ run_id: None, ++ user_layer: load_settings_user()?, + user_settings_path: Some(active_settings_path(None)), + })?; + let client = ctx.server().await?; +diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs +index 49ab984c..2db76d56 100644 +--- a/lib/crates/fabro-cli/src/commands/run/attach.rs ++++ b/lib/crates/fabro-cli/src/commands/run/attach.rs +@@ -119,10 +119,10 @@ pub(crate) async fn attach_run_with_client( + } + + struct AttachOptions { +- auto_approve: bool, +- verbose: bool, ++ auto_approve: bool, ++ verbose: bool, + kill_on_detach: bool, +- json_output: bool, ++ json_output: bool, + } + + fn replay_run_with_client( +@@ -295,7 +295,7 @@ fn api_question_to_question(question: &types::ApiQuestion) -> Question { + .options + .iter() + .map(|option| QuestionOption { +- key: option.key.clone(), ++ key: option.key.clone(), + label: option.label.clone(), + }) + .collect(); +@@ -571,14 +571,14 @@ mod tests { + #[test] + fn answer_requires_reattach_for_interrupted_and_skipped_answers() { + let interrupted = Answer { +- value: AnswerValue::Interrupted, ++ value: AnswerValue::Interrupted, + selected_option: None, +- text: None, ++ text: None, + }; + let skipped = Answer { +- value: AnswerValue::Skipped, ++ value: AnswerValue::Skipped, + selected_option: None, +- text: None, ++ text: None, + }; + let answered = Answer::yes(); + +diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs +index 76a324b3..bc18ac01 100644 +--- a/lib/crates/fabro-cli/src/commands/run/cp.rs ++++ b/lib/crates/fabro-cli/src/commands/run/cp.rs +@@ -16,13 +16,13 @@ use crate::shared::{print_json_pretty, split_run_path}; + #[derive(Debug)] + enum CopyDirection { + Download { +- run_prefix: String, ++ run_prefix: String, + remote_path: String, +- local_path: PathBuf, ++ local_path: PathBuf, + }, + Upload { +- local_path: PathBuf, +- run_prefix: String, ++ local_path: PathBuf, ++ run_prefix: String, + remote_path: String, + }, + } +@@ -110,13 +110,13 @@ fn parse_direction(src: &str, dst: &str) -> Result { + + match (src_parts, dst_parts) { + (Some((run_prefix, remote_path)), None) => Ok(CopyDirection::Download { +- run_prefix: run_prefix.to_string(), ++ run_prefix: run_prefix.to_string(), + remote_path: remote_path.to_string(), +- local_path: PathBuf::from(dst), ++ local_path: PathBuf::from(dst), + }), + (None, Some((run_prefix, remote_path))) => Ok(CopyDirection::Upload { +- local_path: PathBuf::from(src), +- run_prefix: run_prefix.to_string(), ++ local_path: PathBuf::from(src), ++ run_prefix: run_prefix.to_string(), + remote_path: remote_path.to_string(), + }), + (Some(_), Some(_)) => { +diff --git a/lib/crates/fabro-cli/src/commands/run/create.rs b/lib/crates/fabro-cli/src/commands/run/create.rs +index 8019aa27..8bd0a750 100644 +--- a/lib/crates/fabro-cli/src/commands/run/create.rs ++++ b/lib/crates/fabro-cli/src/commands/run/create.rs +@@ -16,7 +16,7 @@ use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, run_manife + use crate::user_config::{self, ServerTarget}; + + pub(crate) struct CreatedRun { +- pub(crate) run_id: RunId, ++ pub(crate) run_id: RunId, + pub(crate) local_run_dir: Option, + } + +diff --git a/lib/crates/fabro-cli/src/commands/run/fork.rs b/lib/crates/fabro-cli/src/commands/run/fork.rs +index facd3ff3..8bf6ec2c 100644 +--- a/lib/crates/fabro-cli/src/commands/run/fork.rs ++++ b/lib/crates/fabro-cli/src/commands/run/fork.rs +@@ -49,11 +49,14 @@ pub(crate) async fn run( + .as_deref() + .map(str::parse::) + .transpose()?; +- let new_run_id = fork(&store, &ForkRunInput { +- source_run_id: run_id, +- target, +- push: !args.no_push, +- })?; ++ let new_run_id = fork( ++ &store, ++ &ForkRunInput { ++ source_run_id: run_id, ++ target, ++ push: !args.no_push, ++ }, ++ )?; + + let run_id_string = run_id.to_string(); + let new_run_id_string = new_run_id.to_string(); +diff --git a/lib/crates/fabro-cli/src/commands/run/output.rs b/lib/crates/fabro-cli/src/commands/run/output.rs +index 8531b0e4..a17a2505 100644 +--- a/lib/crates/fabro-cli/src/commands/run/output.rs ++++ b/lib/crates/fabro-cli/src/commands/run/output.rs +@@ -73,19 +73,19 @@ pub(crate) fn print_preflight_workflow_summary( + + fn api_diagnostic_to_local(diagnostic: &types::WorkflowDiagnostic) -> fabro_validate::Diagnostic { + fabro_validate::Diagnostic { +- rule: diagnostic.rule.clone(), ++ rule: diagnostic.rule.clone(), + severity: match diagnostic.severity { + types::WorkflowDiagnosticSeverity::Error => fabro_validate::Severity::Error, + types::WorkflowDiagnosticSeverity::Warning => fabro_validate::Severity::Warning, + types::WorkflowDiagnosticSeverity::Info => fabro_validate::Severity::Info, + }, +- message: diagnostic.message.clone(), +- node_id: diagnostic.node_id.clone(), +- edge: diagnostic ++ message: diagnostic.message.clone(), ++ node_id: diagnostic.node_id.clone(), ++ edge: diagnostic + .edge + .as_ref() + .map(|edge| (edge[0].clone(), edge[1].clone())), +- fix: diagnostic.fix.clone(), ++ fix: diagnostic.fix.clone(), + } + } + +@@ -97,24 +97,24 @@ pub(crate) fn api_diagnostics_to_local( + + pub(crate) fn api_check_report_to_local(report: &types::PreflightCheckReport) -> CheckReport { + CheckReport { +- title: report.title.clone(), ++ title: report.title.clone(), + sections: report + .sections + .iter() + .map(|section| CheckSection { +- title: section.title.clone(), ++ title: section.title.clone(), + checks: section + .checks + .iter() + .map(|check| CheckResult { +- name: check.name.clone(), +- status: match check.status { ++ name: check.name.clone(), ++ status: match check.status { + types::PreflightCheckResultStatus::Pass => CheckStatus::Pass, + types::PreflightCheckResultStatus::Warning => CheckStatus::Warning, + types::PreflightCheckResultStatus::Error => CheckStatus::Error, + }, +- summary: check.summary.clone(), +- details: check ++ summary: check.summary.clone(), ++ details: check + .details + .iter() + .map(|detail| CheckDetail { +diff --git a/lib/crates/fabro-cli/src/commands/run/overrides.rs b/lib/crates/fabro-cli/src/commands/run/overrides.rs +index 9a8a813b..36daa69b 100644 +--- a/lib/crates/fabro-cli/src/commands/run/overrides.rs ++++ b/lib/crates/fabro-cli/src/commands/run/overrides.rs +@@ -30,8 +30,8 @@ fn model_from_args(model: Option<&str>, provider: Option<&str>) -> Option, + } + +@@ -63,11 +63,14 @@ pub(crate) async fn run( + + let target = args.target.as_deref().unwrap().parse::()?; + +- rewind(&store, &RewindInput { +- run_id, +- target: target.clone(), +- push: !args.no_push, +- })?; ++ rewind( ++ &store, ++ &RewindInput { ++ run_id, ++ target: target.clone(), ++ push: !args.no_push, ++ }, ++ )?; + let entry = timeline.resolve(&target)?; + reset_rewound_run_state(lookup.client(), &store, &run_id, entry).await?; + +@@ -94,9 +97,9 @@ pub(crate) fn timeline_entries_json(timeline: &RunTimeline) -> Vec, ++ pub(super) cost: Option, + } + + impl ProgressUsage { + pub(super) fn from_stage_usage(usage: &BilledModelUsage) -> Option { + let tokens = usage.tokens(); + Some(Self { +- input_tokens: u64::try_from(tokens.input_tokens).ok()?, ++ input_tokens: u64::try_from(tokens.input_tokens).ok()?, + output_tokens: u64::try_from(tokens.billable_output_tokens()).ok()?, +- cost: usage.total_usd_micros.map(|cost| cost as f64 / 1_000_000.0), ++ cost: usage.total_usd_micros.map(|cost| cost as f64 / 1_000_000.0), + }) + } + +@@ -35,8 +35,8 @@ impl ProgressUsage { + pub(super) enum ProgressEvent { + WorkflowStarted { + worktree_dir: Option, +- base_branch: Option, +- base_sha: Option, ++ base_branch: Option, ++ base_sha: Option, + }, + WorkingDirectorySet { + working_directory: String, +@@ -45,12 +45,12 @@ pub(super) enum ProgressEvent { + provider: String, + }, + SandboxReady { +- provider: String, ++ provider: String, + duration_ms: u64, +- name: Option, +- cpu: Option, +- memory: Option, +- url: Option, ++ name: Option, ++ cpu: Option, ++ memory: Option, ++ url: Option, + }, + SshAccessReady { + ssh_command: String, +@@ -62,98 +62,98 @@ pub(super) enum ProgressEvent { + duration_ms: u64, + }, + SetupCommandCompleted { +- command: String, ++ command: String, + command_index: u64, +- exit_code: i64, +- duration_ms: u64, ++ exit_code: i64, ++ duration_ms: u64, + }, + CliEnsureStarted { + cli_name: String, + }, + CliEnsureCompleted { +- cli_name: String, ++ cli_name: String, + already_installed: bool, +- duration_ms: u64, ++ duration_ms: u64, + }, + CliEnsureFailed { + cli_name: String, + }, + DevcontainerResolved { +- dockerfile_lines: u64, +- environment_count: u64, ++ dockerfile_lines: u64, ++ environment_count: u64, + lifecycle_command_count: u64, +- workspace_folder: String, ++ workspace_folder: String, + }, + DevcontainerLifecycleStarted { +- phase: String, ++ phase: String, + command_count: u64, + }, + DevcontainerLifecycleCompleted { +- phase: String, ++ phase: String, + duration_ms: u64, + }, + DevcontainerLifecycleFailed { +- phase: String, +- command: String, ++ phase: String, ++ command: String, + exit_code: i64, +- stderr: String, ++ stderr: String, + }, + DevcontainerLifecycleCommandCompleted { +- command: String, ++ command: String, + command_index: u64, +- exit_code: i64, +- duration_ms: u64, ++ exit_code: i64, ++ duration_ms: u64, + }, + StageStarted { + node_id: String, +- name: String, +- script: Option, ++ name: String, ++ script: Option, + }, + StageCompleted { +- node_id: String, +- name: String, ++ node_id: String, ++ name: String, + duration_ms: u64, +- status: String, +- usage: Option, ++ status: String, ++ usage: Option, + }, + StageFailed { + node_id: String, +- name: String, +- error: String, ++ name: String, ++ error: String, + }, + StageRetrying { +- name: String, +- attempt: u64, ++ name: String, ++ attempt: u64, + max_attempts: u64, +- delay_ms: u64, ++ delay_ms: u64, + }, + ParallelStarted, + ParallelBranchStarted { + branch: String, + }, + ParallelBranchCompleted { +- branch: String, ++ branch: String, + duration_ms: u64, +- status: String, ++ status: String, + }, + ParallelCompleted, + AssistantMessage { + stage_node_id: String, +- model: String, ++ model: String, + }, + ToolCallStarted { + stage_node_id: String, +- tool_name: String, +- tool_call_id: String, +- arguments: Value, +- timestamp: Option>, ++ tool_name: String, ++ tool_call_id: String, ++ arguments: Value, ++ timestamp: Option>, + }, + ToolCallCompleted { + stage_node_id: String, +- tool_call_id: String, +- is_error: bool, +- duration_ms: Option, +- timestamp: Option>, ++ tool_call_id: String, ++ is_error: bool, ++ duration_ms: Option, ++ timestamp: Option>, + }, + ContextWindowWarning { + stage_node_id: String, +@@ -163,38 +163,38 @@ pub(super) enum ProgressEvent { + stage_node_id: String, + }, + CompactionCompleted { +- stage_node_id: String, +- original_turn_count: u64, ++ stage_node_id: String, ++ original_turn_count: u64, + preserved_turn_count: u64, +- tracked_file_count: u64, ++ tracked_file_count: u64, + }, + LlmRetry { + stage_node_id: String, +- model: String, +- attempt: u64, +- delay_ms: u64, +- error: String, ++ model: String, ++ attempt: u64, ++ delay_ms: u64, ++ error: String, + }, + SubagentSpawned { + stage_node_id: String, +- agent_id: String, +- task: String, ++ agent_id: String, ++ task: String, + }, + SubagentCompleted { + stage_node_id: String, +- agent_id: String, +- success: bool, +- turns_used: u64, ++ agent_id: String, ++ success: bool, ++ turns_used: u64, + }, + EdgeSelected { + from_node: String, +- to_node: String, +- label: Option, ++ to_node: String, ++ label: Option, + condition: Option, + }, + LoopRestart { + from_node: String, +- to_node: String, ++ to_node: String, + }, + RetroStarted, + RetroCompleted { +@@ -204,13 +204,13 @@ pub(super) enum ProgressEvent { + duration_ms: u64, + }, + RunNotice { +- level: RunNoticeLevel, +- code: String, ++ level: RunNoticeLevel, ++ code: String, + message: String, + }, + PullRequestCreated { + pr_url: String, +- draft: bool, ++ draft: bool, + }, + PullRequestFailed { + error: String, +@@ -224,8 +224,8 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { + match &stored.body { + EventBody::RunStarted(props) => Some(ProgressEvent::WorkflowStarted { + worktree_dir: props.worktree_dir.clone(), +- base_branch: props.base_branch.clone(), +- base_sha: props.base_sha.clone(), ++ base_branch: props.base_branch.clone(), ++ base_sha: props.base_sha.clone(), + }), + EventBody::SandboxInitialized(props) => Some(ProgressEvent::WorkingDirectorySet { + working_directory: props.working_directory.clone(), +@@ -234,12 +234,12 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { + provider: props.provider.clone(), + }), + EventBody::SandboxReady(props) => Some(ProgressEvent::SandboxReady { +- provider: props.provider.clone(), ++ provider: props.provider.clone(), + duration_ms: props.duration_ms, +- name: props.name.clone(), +- cpu: props.cpu, +- memory: props.memory, +- url: props.url.clone(), ++ name: props.name.clone(), ++ cpu: props.cpu, ++ memory: props.memory, ++ url: props.url.clone(), + }), + EventBody::SshAccessReady(props) => Some(ProgressEvent::SshAccessReady { + ssh_command: props.ssh_command.clone(), +@@ -251,54 +251,54 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { + duration_ms: props.duration_ms, + }), + EventBody::SetupCommandCompleted(props) => Some(ProgressEvent::SetupCommandCompleted { +- command: props.command.clone(), ++ command: props.command.clone(), + command_index: props.index as u64, +- exit_code: i64::from(props.exit_code), +- duration_ms: props.duration_ms, ++ exit_code: i64::from(props.exit_code), ++ duration_ms: props.duration_ms, + }), + EventBody::CliEnsureStarted(props) => Some(ProgressEvent::CliEnsureStarted { + cli_name: props.cli_name.clone(), + }), + EventBody::CliEnsureCompleted(props) => Some(ProgressEvent::CliEnsureCompleted { +- cli_name: props.cli_name.clone(), ++ cli_name: props.cli_name.clone(), + already_installed: props.already_installed, +- duration_ms: props.duration_ms, ++ duration_ms: props.duration_ms, + }), + EventBody::CliEnsureFailed(props) => Some(ProgressEvent::CliEnsureFailed { + cli_name: props.cli_name.clone(), + }), + EventBody::DevcontainerResolved(props) => Some(ProgressEvent::DevcontainerResolved { +- dockerfile_lines: props.dockerfile_lines as u64, +- environment_count: props.environment_count as u64, ++ dockerfile_lines: props.dockerfile_lines as u64, ++ environment_count: props.environment_count as u64, + lifecycle_command_count: props.lifecycle_command_count as u64, +- workspace_folder: props.workspace_folder.clone(), ++ workspace_folder: props.workspace_folder.clone(), + }), + EventBody::DevcontainerLifecycleStarted(props) => { + Some(ProgressEvent::DevcontainerLifecycleStarted { +- phase: props.phase.clone(), ++ phase: props.phase.clone(), + command_count: props.command_count as u64, + }) + } + EventBody::DevcontainerLifecycleCompleted(props) => { + Some(ProgressEvent::DevcontainerLifecycleCompleted { +- phase: props.phase.clone(), ++ phase: props.phase.clone(), + duration_ms: props.duration_ms, + }) + } + EventBody::DevcontainerLifecycleFailed(props) => { + Some(ProgressEvent::DevcontainerLifecycleFailed { +- phase: props.phase.clone(), +- command: props.command.clone(), ++ phase: props.phase.clone(), ++ command: props.command.clone(), + exit_code: i64::from(props.exit_code), +- stderr: props.stderr.clone(), ++ stderr: props.stderr.clone(), + }) + } + EventBody::DevcontainerLifecycleCommandCompleted(props) => { + Some(ProgressEvent::DevcontainerLifecycleCommandCompleted { +- command: props.command.clone(), ++ command: props.command.clone(), + command_index: props.index as u64, +- exit_code: i64::from(props.exit_code), +- duration_ms: props.duration_ms, ++ exit_code: i64::from(props.exit_code), ++ duration_ms: props.duration_ms, + }) + } + EventBody::StageStarted(_) => Some(ProgressEvent::StageStarted { +@@ -325,38 +325,38 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { + ), + }), + EventBody::StageRetrying(props) => Some(ProgressEvent::StageRetrying { +- name: node_label, +- attempt: props.attempt as u64, ++ name: node_label, ++ attempt: props.attempt as u64, + max_attempts: props.max_attempts as u64, +- delay_ms: props.delay_ms, ++ delay_ms: props.delay_ms, + }), + EventBody::ParallelStarted(_) => Some(ProgressEvent::ParallelStarted), + EventBody::ParallelBranchStarted(_) => { + Some(ProgressEvent::ParallelBranchStarted { branch: node_id }) + } + EventBody::ParallelBranchCompleted(props) => Some(ProgressEvent::ParallelBranchCompleted { +- branch: node_id, ++ branch: node_id, + duration_ms: props.duration_ms, +- status: props.status.clone(), ++ status: props.status.clone(), + }), + EventBody::ParallelCompleted(_) => Some(ProgressEvent::ParallelCompleted), + EventBody::AgentMessage(props) => Some(ProgressEvent::AssistantMessage { + stage_node_id: node_id, +- model: props.model.clone(), ++ model: props.model.clone(), + }), + EventBody::AgentToolStarted(props) => Some(ProgressEvent::ToolCallStarted { + stage_node_id: node_id, +- tool_name: props.tool_name.clone(), +- tool_call_id: props.tool_call_id.clone(), +- arguments: props.arguments.clone(), +- timestamp: Some(stored.ts), ++ tool_name: props.tool_name.clone(), ++ tool_call_id: props.tool_call_id.clone(), ++ arguments: props.arguments.clone(), ++ timestamp: Some(stored.ts), + }), + EventBody::AgentToolCompleted(props) => Some(ProgressEvent::ToolCallCompleted { + stage_node_id: node_id, +- tool_call_id: props.tool_call_id.clone(), +- is_error: props.is_error, +- duration_ms: None, +- timestamp: Some(stored.ts), ++ tool_call_id: props.tool_call_id.clone(), ++ is_error: props.is_error, ++ duration_ms: None, ++ timestamp: Some(stored.ts), + }), + EventBody::AgentWarning(props) if props.kind == "context_window" => { + let usage_percent = props +@@ -374,10 +374,10 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { + stage_node_id: node_id, + }), + EventBody::AgentCompactionCompleted(props) => Some(ProgressEvent::CompactionCompleted { +- stage_node_id: node_id, +- original_turn_count: props.original_turn_count as u64, ++ stage_node_id: node_id, ++ original_turn_count: props.original_turn_count as u64, + preserved_turn_count: props.preserved_turn_count as u64, +- tracked_file_count: props.tracked_file_count as u64, ++ tracked_file_count: props.tracked_file_count as u64, + }), + EventBody::AgentLlmRetry(props) => { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] +@@ -392,24 +392,24 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { + } + EventBody::AgentSubSpawned(props) => Some(ProgressEvent::SubagentSpawned { + stage_node_id: node_id, +- agent_id: props.agent_id.clone(), +- task: props.task.clone(), ++ agent_id: props.agent_id.clone(), ++ task: props.task.clone(), + }), + EventBody::AgentSubCompleted(props) => Some(ProgressEvent::SubagentCompleted { + stage_node_id: node_id, +- agent_id: props.agent_id.clone(), +- success: props.success, +- turns_used: props.turns_used as u64, ++ agent_id: props.agent_id.clone(), ++ success: props.success, ++ turns_used: props.turns_used as u64, + }), + EventBody::EdgeSelected(props) => Some(ProgressEvent::EdgeSelected { + from_node: props.from_node.clone(), +- to_node: props.to_node.clone(), +- label: props.label.clone(), ++ to_node: props.to_node.clone(), ++ label: props.label.clone(), + condition: props.condition.clone(), + }), + EventBody::LoopRestart(props) => Some(ProgressEvent::LoopRestart { + from_node: props.from_node.clone(), +- to_node: props.to_node.clone(), ++ to_node: props.to_node.clone(), + }), + EventBody::RetroStarted(_) => Some(ProgressEvent::RetroStarted), + EventBody::RetroCompleted(props) => Some(ProgressEvent::RetroCompleted { +@@ -419,13 +419,13 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option { + duration_ms: props.duration_ms, + }), + EventBody::RunNotice(props) => Some(ProgressEvent::RunNotice { +- level: props.level, +- code: props.code.clone(), ++ level: props.level, ++ code: props.code.clone(), + message: props.message.clone(), + }), + EventBody::PullRequestCreated(props) => Some(ProgressEvent::PullRequestCreated { + pr_url: props.pr_url.clone(), +- draft: props.draft, ++ draft: props.draft, + }), + EventBody::PullRequestFailed(props) => Some(ProgressEvent::PullRequestFailed { + error: props.error.clone(), +@@ -477,17 +477,20 @@ mod tests { + + #[test] + fn parse_edge_selected() { +- let stored = to_run_event(&fixtures::RUN_1, &Event::EdgeSelected { +- from_node: "a".into(), +- to_node: "b".into(), +- label: Some("yes".into()), +- condition: None, +- reason: "condition".into(), +- preferred_label: None, +- suggested_next_ids: Vec::new(), +- stage_status: "success".into(), +- is_jump: false, +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_1, ++ &Event::EdgeSelected { ++ from_node: "a".into(), ++ to_node: "b".into(), ++ label: Some("yes".into()), ++ condition: None, ++ reason: "condition".into(), ++ preferred_label: None, ++ suggested_next_ids: Vec::new(), ++ stage_status: "success".into(), ++ is_jump: false, ++ }, ++ ); + + let event = from_run_event(&stored).unwrap(); + assert!(matches!( +@@ -542,14 +545,14 @@ mod tests { + #[test] + fn round_trip_agent_tool_call() { + let event = Event::Agent { +- stage: "code".into(), +- visit: 1, +- event: AgentEvent::ToolCallStarted { +- tool_name: "read_file".into(), ++ stage: "code".into(), ++ visit: 1, ++ event: AgentEvent::ToolCallStarted { ++ tool_name: "read_file".into(), + tool_call_id: "tc1".into(), +- arguments: serde_json::json!({"path": "src/main.rs"}), ++ arguments: serde_json::json!({"path": "src/main.rs"}), + }, +- session_id: None, ++ session_id: None, + parent_session_id: None, + }; + +@@ -631,12 +634,12 @@ mod tests { + fn round_trip_sandbox_ready() { + let event = Event::Sandbox { + event: fabro_agent::SandboxEvent::Ready { +- provider: "daytona".into(), ++ provider: "daytona".into(), + duration_ms: 2500, +- name: Some("sandbox-1".into()), +- cpu: Some(4.0), +- memory: Some(8.0), +- url: Some("https://example.test".into()), ++ name: Some("sandbox-1".into()), ++ cpu: Some(4.0), ++ memory: Some(8.0), ++ url: Some("https://example.test".into()), + }, + }; + +@@ -656,8 +659,8 @@ mod tests { + #[test] + fn round_trip_run_notice() { + let event = Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "sandbox_cleanup_failed".into(), ++ level: RunNoticeLevel::Warn, ++ code: "sandbox_cleanup_failed".into(), + message: "sandbox cleanup failed".into(), + }; + +diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs +index 4640061f..6670eed8 100644 +--- a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs ++++ b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs +@@ -15,9 +15,9 @@ use stage_display::StageDisplay; + + pub(crate) struct ProgressUI { + renderer: ProgressRenderer, +- stage: StageDisplay, +- setup: SetupDisplay, +- info: InfoDisplay, ++ stage: StageDisplay, ++ setup: SetupDisplay, ++ info: InfoDisplay, + } + + impl ProgressUI { +@@ -484,22 +484,25 @@ mod tests { + + fn stage_started(node_id: &str, name: &str) -> Event { + Event::StageStarted { +- node_id: node_id.into(), +- name: name.into(), +- index: 0, ++ node_id: node_id.into(), ++ name: name.into(), ++ index: 0, + handler_type: String::new(), +- attempt: 1, ++ attempt: 1, + max_attempts: 1, + } + } + + fn assistant_message(stage: &str, model: &str) -> Event { +- agent_event(stage, AgentEvent::AssistantMessage { +- text: "done".into(), +- model: model.into(), +- usage: TokenCounts::default(), +- tool_call_count: 0, +- }) ++ agent_event( ++ stage, ++ AgentEvent::AssistantMessage { ++ text: "done".into(), ++ model: model.into(), ++ usage: TokenCounts::default(), ++ tool_call_count: 0, ++ }, ++ ) + } + + fn stage_completed(node_id: &str, name: &str) -> Event { +@@ -544,20 +547,26 @@ mod tests { + assert!(ui.stage.active_stages.contains_key("fork1")); + assert!(ui.stage.parallel_parent.is_none()); + +- emit(&mut ui, Event::ParallelStarted { +- node_id: "fork1".into(), +- visit: 1, +- branch_count: 2, +- join_policy: "wait_all".into(), +- }); ++ emit( ++ &mut ui, ++ Event::ParallelStarted { ++ node_id: "fork1".into(), ++ visit: 1, ++ branch_count: 2, ++ join_policy: "wait_all".into(), ++ }, ++ ); + assert_eq!(ui.stage.parallel_parent.as_deref(), Some("fork1")); + +- emit(&mut ui, Event::ParallelBranchStarted { +- parallel_group_id: StageId::new("fork1", 1), +- parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), +- branch: "security".into(), +- index: 0, +- }); ++ emit( ++ &mut ui, ++ Event::ParallelBranchStarted { ++ parallel_group_id: StageId::new("fork1", 1), ++ parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), ++ branch: "security".into(), ++ index: 0, ++ }, ++ ); + let stage = &ui.stage.active_stages["fork1"]; + assert_eq!(stage.tool_calls.len(), 1); + assert_eq!(stage.tool_calls[0].tool_call_id, "security"); +@@ -566,15 +575,18 @@ mod tests { + ToolCallStatus::Running + )); + +- emit(&mut ui, Event::ParallelBranchCompleted { +- parallel_group_id: StageId::new("fork1", 1), +- parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), +- branch: "security".into(), +- index: 0, +- duration_ms: 2000, +- status: "success".into(), +- head_sha: None, +- }); ++ emit( ++ &mut ui, ++ Event::ParallelBranchCompleted { ++ parallel_group_id: StageId::new("fork1", 1), ++ parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), ++ branch: "security".into(), ++ index: 0, ++ duration_ms: 2000, ++ status: "success".into(), ++ head_sha: None, ++ }, ++ ); + let stage = &ui.stage.active_stages["fork1"]; + assert!(matches!( + stage.tool_calls[0].status, +@@ -587,18 +599,24 @@ mod tests { + let mut ui = ProgressUI::new(true, false); + + emit(&mut ui, stage_started("fork1", "Fork")); +- emit(&mut ui, Event::ParallelStarted { +- node_id: "fork1".into(), +- visit: 1, +- branch_count: 1, +- join_policy: "wait_all".into(), +- }); +- emit(&mut ui, Event::ParallelBranchStarted { +- parallel_group_id: StageId::new("fork1", 1), +- parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), +- branch: "security".into(), +- index: 0, +- }); ++ emit( ++ &mut ui, ++ Event::ParallelStarted { ++ node_id: "fork1".into(), ++ visit: 1, ++ branch_count: 1, ++ join_policy: "wait_all".into(), ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::ParallelBranchStarted { ++ parallel_group_id: StageId::new("fork1", 1), ++ parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), ++ branch: "security".into(), ++ index: 0, ++ }, ++ ); + + let stage = &ui.stage.active_stages["fork1"]; + let message = stage.tool_calls[0].bar.message(); +@@ -617,21 +635,27 @@ mod tests { + + emit( + &mut ui, +- agent_event("s1", AgentEvent::CompactionStarted { +- estimated_tokens: 5000, +- context_window_size: 8000, +- }), ++ agent_event( ++ "s1", ++ AgentEvent::CompactionStarted { ++ estimated_tokens: 5000, ++ context_window_size: 8000, ++ }, ++ ), + ); + assert!(ui.stage.active_stages["s1"].compaction_bar.is_some()); + + emit( + &mut ui, +- agent_event("s1", AgentEvent::CompactionCompleted { +- original_turn_count: 20, +- preserved_turn_count: 6, +- summary_token_estimate: 500, +- tracked_file_count: 3, +- }), ++ agent_event( ++ "s1", ++ AgentEvent::CompactionCompleted { ++ original_turn_count: 20, ++ preserved_turn_count: 6, ++ summary_token_estimate: 500, ++ tracked_file_count: 3, ++ }, ++ ), + ); + assert!(ui.stage.active_stages["s1"].compaction_bar.is_none()); + } +@@ -650,86 +674,101 @@ mod tests { + let events = vec![ + stage_started("code", "Code"), + Event::SandboxInitialized { +- working_directory: "/home/daytona/workspace".into(), +- provider: "daytona".into(), +- identifier: None, ++ working_directory: "/home/daytona/workspace".into(), ++ provider: "daytona".into(), ++ identifier: None, + host_working_directory: None, +- container_mount_point: None, ++ container_mount_point: None, + }, +- agent_event("code", AgentEvent::ToolCallStarted { +- tool_name: "read_file".into(), +- tool_call_id: "tc1".into(), +- arguments: serde_json::json!({ +- "file_path": "/home/daytona/workspace/src/main.rs" +- }), +- }), ++ agent_event( ++ "code", ++ AgentEvent::ToolCallStarted { ++ tool_name: "read_file".into(), ++ tool_call_id: "tc1".into(), ++ arguments: serde_json::json!({ ++ "file_path": "/home/daytona/workspace/src/main.rs" ++ }), ++ }, ++ ), + assistant_message("code", "gpt-5-mini"), + Event::EdgeSelected { +- from_node: "code".into(), +- to_node: "review".into(), +- label: Some("ship".into()), +- condition: None, +- reason: "condition".into(), +- preferred_label: None, ++ from_node: "code".into(), ++ to_node: "review".into(), ++ label: Some("ship".into()), ++ condition: None, ++ reason: "condition".into(), ++ preferred_label: None, + suggested_next_ids: Vec::new(), +- stage_status: "success".into(), +- is_jump: false, ++ stage_status: "success".into(), ++ is_jump: false, + }, + Event::StageRetrying { +- node_id: "code".into(), +- name: "Code".into(), +- index: 0, +- attempt: 2, ++ node_id: "code".into(), ++ name: "Code".into(), ++ index: 0, ++ attempt: 2, + max_attempts: 3, +- delay_ms: 1500, ++ delay_ms: 1500, + }, +- agent_event("code", AgentEvent::Warning { +- kind: "context_window".into(), +- message: "high usage".into(), +- details: serde_json::json!({"usage_percent": 92}), +- }), +- agent_event("code", AgentEvent::LlmRetry { +- provider: "openai".into(), +- model: "gpt-5-mini".into(), +- attempt: 2, +- delay_secs: 1.5, +- error: fabro_llm::Error::Configuration { +- message: "busy".into(), +- source: None, ++ agent_event( ++ "code", ++ AgentEvent::Warning { ++ kind: "context_window".into(), ++ message: "high usage".into(), ++ details: serde_json::json!({"usage_percent": 92}), + }, +- }), +- agent_event("code", AgentEvent::SubAgentSpawned { +- agent_id: "a1".into(), +- depth: 1, +- task: "review recent changes".into(), +- }), +- agent_event("code", AgentEvent::SubAgentCompleted { +- agent_id: "a1".into(), +- depth: 1, +- success: true, +- turns_used: 3, +- }), ++ ), ++ agent_event( ++ "code", ++ AgentEvent::LlmRetry { ++ provider: "openai".into(), ++ model: "gpt-5-mini".into(), ++ attempt: 2, ++ delay_secs: 1.5, ++ error: fabro_llm::Error::Configuration { ++ message: "busy".into(), ++ source: None, ++ }, ++ }, ++ ), ++ agent_event( ++ "code", ++ AgentEvent::SubAgentSpawned { ++ agent_id: "a1".into(), ++ depth: 1, ++ task: "review recent changes".into(), ++ }, ++ ), ++ agent_event( ++ "code", ++ AgentEvent::SubAgentCompleted { ++ agent_id: "a1".into(), ++ depth: 1, ++ success: true, ++ turns_used: 3, ++ }, ++ ), + Event::SetupStarted { command_count: 1 }, + Event::SetupCommandCompleted { +- command: "bun install".into(), +- index: 0, +- exit_code: 0, ++ command: "bun install".into(), ++ index: 0, ++ exit_code: 0, + duration_ms: 2200, + }, + Event::SetupCompleted { duration_ms: 2200 }, + Event::DevcontainerLifecycleStarted { +- phase: "postCreate".into(), ++ phase: "postCreate".into(), + command_count: 1, + }, + Event::DevcontainerLifecycleCommandCompleted { +- phase: "postCreate".into(), +- command: "npm run setup".into(), +- index: 0, +- exit_code: 0, ++ phase: "postCreate".into(), ++ command: "npm run setup".into(), ++ index: 0, ++ exit_code: 0, + duration_ms: 1400, + }, + Event::DevcontainerLifecycleCompleted { +- phase: "postCreate".into(), ++ phase: "postCreate".into(), + duration_ms: 1400, + }, + ]; +@@ -756,20 +795,26 @@ mod tests { + emit(&mut ui, assistant_message("plan", "gpt-5-mini")); + emit( + &mut ui, +- agent_event("plan", AgentEvent::ToolCallStarted { +- tool_name: "read_file".into(), +- tool_call_id: "tc1".into(), +- arguments: serde_json::json!({"path": "src/main.rs"}), +- }), ++ agent_event( ++ "plan", ++ AgentEvent::ToolCallStarted { ++ tool_name: "read_file".into(), ++ tool_call_id: "tc1".into(), ++ arguments: serde_json::json!({"path": "src/main.rs"}), ++ }, ++ ), + ); + emit( + &mut ui, +- agent_event("plan", AgentEvent::ToolCallCompleted { +- tool_name: "read_file".into(), +- tool_call_id: "tc1".into(), +- output: serde_json::json!({"ok": true}), +- is_error: false, +- }), ++ agent_event( ++ "plan", ++ AgentEvent::ToolCallCompleted { ++ tool_name: "read_file".into(), ++ tool_call_id: "tc1".into(), ++ output: serde_json::json!({"ok": true}), ++ is_error: false, ++ }, ++ ), + ); + emit(&mut ui, stage_completed("plan", "Plan")); + +@@ -780,47 +825,68 @@ mod tests { + fn plain_default_setup_snapshot() { + let (mut ui, buffer) = capture_ui(false); + +- emit(&mut ui, Event::Sandbox { +- event: SandboxEvent::Initializing { +- provider: "daytona".into(), ++ emit( ++ &mut ui, ++ Event::Sandbox { ++ event: SandboxEvent::Initializing { ++ provider: "daytona".into(), ++ }, ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::Sandbox { ++ event: SandboxEvent::Ready { ++ provider: "daytona".into(), ++ duration_ms: 2500, ++ name: Some("sandbox-1".into()), ++ cpu: Some(4.0), ++ memory: Some(8.0), ++ url: None, ++ }, + }, +- }); +- emit(&mut ui, Event::Sandbox { +- event: SandboxEvent::Ready { +- provider: "daytona".into(), +- duration_ms: 2500, +- name: Some("sandbox-1".into()), +- cpu: Some(4.0), +- memory: Some(8.0), +- url: None, ++ ); ++ emit( ++ &mut ui, ++ Event::SshAccessReady { ++ ssh_command: "ssh daytona@example".into(), + }, +- }); +- emit(&mut ui, Event::SshAccessReady { +- ssh_command: "ssh daytona@example".into(), +- }); ++ ); + emit(&mut ui, Event::SetupStarted { command_count: 2 }); + emit(&mut ui, Event::SetupCompleted { duration_ms: 8200 }); +- emit(&mut ui, Event::CliEnsureCompleted { +- cli_name: "gh".into(), +- provider: "github".into(), +- already_installed: false, +- node_installed: false, +- duration_ms: 600, +- }); +- emit(&mut ui, Event::DevcontainerResolved { +- dockerfile_lines: 24, +- environment_count: 3, +- lifecycle_command_count: 2, +- workspace_folder: "/workspace".into(), +- }); +- emit(&mut ui, Event::DevcontainerLifecycleStarted { +- phase: "postCreate".into(), +- command_count: 2, +- }); +- emit(&mut ui, Event::DevcontainerLifecycleCompleted { +- phase: "postCreate".into(), +- duration_ms: 1800, +- }); ++ emit( ++ &mut ui, ++ Event::CliEnsureCompleted { ++ cli_name: "gh".into(), ++ provider: "github".into(), ++ already_installed: false, ++ node_installed: false, ++ duration_ms: 600, ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::DevcontainerResolved { ++ dockerfile_lines: 24, ++ environment_count: 3, ++ lifecycle_command_count: 2, ++ workspace_folder: "/workspace".into(), ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::DevcontainerLifecycleStarted { ++ phase: "postCreate".into(), ++ command_count: 2, ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::DevcontainerLifecycleCompleted { ++ phase: "postCreate".into(), ++ duration_ms: 1800, ++ }, ++ ); + + insta::assert_snapshot!(rendered(&buffer), @r" + Sandbox: daytona (ready in 2s) +@@ -840,104 +906,140 @@ mod tests { + let (mut ui, buffer) = capture_ui(true); + + emit(&mut ui, stage_started("code", "Code")); +- emit(&mut ui, Event::SandboxInitialized { +- working_directory: "/home/daytona/workspace".into(), +- provider: "daytona".into(), +- identifier: None, +- host_working_directory: None, +- container_mount_point: None, +- }); + emit( + &mut ui, +- agent_event("code", AgentEvent::ToolCallStarted { +- tool_name: "read_file".into(), +- tool_call_id: "tc1".into(), +- arguments: serde_json::json!({ +- "file_path": "/home/daytona/workspace/src/main.rs" +- }), +- }), ++ Event::SandboxInitialized { ++ working_directory: "/home/daytona/workspace".into(), ++ provider: "daytona".into(), ++ identifier: None, ++ host_working_directory: None, ++ container_mount_point: None, ++ }, ++ ); ++ emit( ++ &mut ui, ++ agent_event( ++ "code", ++ AgentEvent::ToolCallStarted { ++ tool_name: "read_file".into(), ++ tool_call_id: "tc1".into(), ++ arguments: serde_json::json!({ ++ "file_path": "/home/daytona/workspace/src/main.rs" ++ }), ++ }, ++ ), + ); + emit(&mut ui, assistant_message("code", "gpt-5-mini")); +- emit(&mut ui, Event::EdgeSelected { +- from_node: "code".into(), +- to_node: "review".into(), +- label: Some("ship".into()), +- condition: None, +- reason: "condition".into(), +- preferred_label: None, +- suggested_next_ids: Vec::new(), +- stage_status: "success".into(), +- is_jump: false, +- }); +- emit(&mut ui, Event::StageRetrying { +- node_id: "code".into(), +- name: "Code".into(), +- index: 0, +- attempt: 2, +- max_attempts: 3, +- delay_ms: 1500, +- }); + emit( + &mut ui, +- agent_event("code", AgentEvent::Warning { +- kind: "context_window".into(), +- message: "high usage".into(), +- details: serde_json::json!({"usage_percent": 92}), +- }), ++ Event::EdgeSelected { ++ from_node: "code".into(), ++ to_node: "review".into(), ++ label: Some("ship".into()), ++ condition: None, ++ reason: "condition".into(), ++ preferred_label: None, ++ suggested_next_ids: Vec::new(), ++ stage_status: "success".into(), ++ is_jump: false, ++ }, + ); + emit( + &mut ui, +- agent_event("code", AgentEvent::LlmRetry { +- provider: "openai".into(), +- model: "gpt-5-mini".into(), +- attempt: 2, +- delay_secs: 1.5, +- error: fabro_llm::Error::Configuration { +- message: "busy".into(), +- source: None, ++ Event::StageRetrying { ++ node_id: "code".into(), ++ name: "Code".into(), ++ index: 0, ++ attempt: 2, ++ max_attempts: 3, ++ delay_ms: 1500, ++ }, ++ ); ++ emit( ++ &mut ui, ++ agent_event( ++ "code", ++ AgentEvent::Warning { ++ kind: "context_window".into(), ++ message: "high usage".into(), ++ details: serde_json::json!({"usage_percent": 92}), + }, +- }), ++ ), + ); + emit( + &mut ui, +- agent_event("code", AgentEvent::SubAgentSpawned { +- agent_id: "a1".into(), +- depth: 1, +- task: "review recent changes".into(), +- }), ++ agent_event( ++ "code", ++ AgentEvent::LlmRetry { ++ provider: "openai".into(), ++ model: "gpt-5-mini".into(), ++ attempt: 2, ++ delay_secs: 1.5, ++ error: fabro_llm::Error::Configuration { ++ message: "busy".into(), ++ source: None, ++ }, ++ }, ++ ), + ); + emit( + &mut ui, +- agent_event("code", AgentEvent::SubAgentCompleted { +- agent_id: "a1".into(), +- depth: 1, +- success: true, +- turns_used: 3, +- }), ++ agent_event( ++ "code", ++ AgentEvent::SubAgentSpawned { ++ agent_id: "a1".into(), ++ depth: 1, ++ task: "review recent changes".into(), ++ }, ++ ), ++ ); ++ emit( ++ &mut ui, ++ agent_event( ++ "code", ++ AgentEvent::SubAgentCompleted { ++ agent_id: "a1".into(), ++ depth: 1, ++ success: true, ++ turns_used: 3, ++ }, ++ ), + ); + emit(&mut ui, Event::SetupStarted { command_count: 1 }); +- emit(&mut ui, Event::SetupCommandCompleted { +- command: "bun install".into(), +- index: 0, +- exit_code: 0, +- duration_ms: 2200, +- }); ++ emit( ++ &mut ui, ++ Event::SetupCommandCompleted { ++ command: "bun install".into(), ++ index: 0, ++ exit_code: 0, ++ duration_ms: 2200, ++ }, ++ ); + emit(&mut ui, Event::SetupCompleted { duration_ms: 2200 }); +- emit(&mut ui, Event::DevcontainerLifecycleStarted { +- phase: "postCreate".into(), +- command_count: 1, +- }); +- emit(&mut ui, Event::DevcontainerLifecycleCommandCompleted { +- phase: "postCreate".into(), +- command: "npm run setup".into(), +- index: 0, +- exit_code: 0, +- duration_ms: 1400, +- }); +- emit(&mut ui, Event::DevcontainerLifecycleCompleted { +- phase: "postCreate".into(), +- duration_ms: 1400, +- }); ++ emit( ++ &mut ui, ++ Event::DevcontainerLifecycleStarted { ++ phase: "postCreate".into(), ++ command_count: 1, ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::DevcontainerLifecycleCommandCompleted { ++ phase: "postCreate".into(), ++ command: "npm run setup".into(), ++ index: 0, ++ exit_code: 0, ++ duration_ms: 1400, ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::DevcontainerLifecycleCompleted { ++ phase: "postCreate".into(), ++ duration_ms: 1400, ++ }, ++ ); + emit(&mut ui, stage_completed("code", "Code")); + + insta::assert_snapshot!(rendered(&buffer), @r#" +@@ -960,24 +1062,33 @@ mod tests { + fn plain_notice_snapshot() { + let (mut ui, buffer) = capture_ui(false); + +- emit(&mut ui, Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "sandbox_cleanup_failed".into(), +- message: "sandbox cleanup failed".into(), +- }); +- emit(&mut ui, Event::PullRequestCreated { +- pr_url: "https://github.com/fabro-sh/fabro/pull/42".into(), +- pr_number: 42, +- owner: "fabro-sh".into(), +- repo: "fabro".into(), +- base_branch: "main".into(), +- head_branch: "fabro/run/42".into(), +- title: "Ship the change".into(), +- draft: true, +- }); +- emit(&mut ui, Event::PullRequestFailed { +- error: "auth token expired".into(), +- }); ++ emit( ++ &mut ui, ++ Event::RunNotice { ++ level: RunNoticeLevel::Warn, ++ code: "sandbox_cleanup_failed".into(), ++ message: "sandbox cleanup failed".into(), ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::PullRequestCreated { ++ pr_url: "https://github.com/fabro-sh/fabro/pull/42".into(), ++ pr_number: 42, ++ owner: "fabro-sh".into(), ++ repo: "fabro".into(), ++ base_branch: "main".into(), ++ head_branch: "fabro/run/42".into(), ++ title: "Ship the change".into(), ++ draft: true, ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::PullRequestFailed { ++ error: "auth token expired".into(), ++ }, ++ ); + + insta::assert_snapshot!(rendered(&buffer), @r" + Warning: sandbox cleanup failed [sandbox_cleanup_failed] +@@ -991,27 +1102,36 @@ mod tests { + let mut ui = ProgressUI::new(true, false); + + emit(&mut ui, stage_started("fork1", "Fork")); +- emit(&mut ui, Event::ParallelStarted { +- node_id: "fork1".into(), +- visit: 1, +- branch_count: 1, +- join_policy: "wait_all".into(), +- }); +- emit(&mut ui, Event::ParallelBranchStarted { +- parallel_group_id: StageId::new("fork1", 1), +- parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), +- branch: "security".into(), +- index: 0, +- }); +- emit(&mut ui, Event::ParallelBranchCompleted { +- parallel_group_id: StageId::new("fork1", 1), +- parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), +- branch: "security".into(), +- index: 0, +- duration_ms: 500, +- status: "success".into(), +- head_sha: None, +- }); ++ emit( ++ &mut ui, ++ Event::ParallelStarted { ++ node_id: "fork1".into(), ++ visit: 1, ++ branch_count: 1, ++ join_policy: "wait_all".into(), ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::ParallelBranchStarted { ++ parallel_group_id: StageId::new("fork1", 1), ++ parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), ++ branch: "security".into(), ++ index: 0, ++ }, ++ ); ++ emit( ++ &mut ui, ++ Event::ParallelBranchCompleted { ++ parallel_group_id: StageId::new("fork1", 1), ++ parallel_branch_id: ParallelBranchId::new(StageId::new("fork1", 1), 0), ++ branch: "security".into(), ++ index: 0, ++ duration_ms: 500, ++ status: "success".into(), ++ head_sha: None, ++ }, ++ ); + + let stage = &ui.stage.active_stages["fork1"]; + assert_eq!(stage.tool_calls[0].bar.prefix(), "500ms"); +@@ -1031,11 +1151,11 @@ mod tests { + let stage_started = serde_json::to_string(&to_run_event_at( + &fixtures::RUN_1, + &Event::StageStarted { +- node_id: "code".into(), +- name: "Code".into(), +- index: 0, ++ node_id: "code".into(), ++ name: "Code".into(), ++ index: 0, + handler_type: "agent".into(), +- attempt: 1, ++ attempt: 1, + max_attempts: 1, + }, + started_ts, +@@ -1044,23 +1164,29 @@ mod tests { + .unwrap(); + let tool_started = serde_json::to_string(&to_run_event_at( + &fixtures::RUN_1, +- &agent_event("code", AgentEvent::ToolCallStarted { +- tool_name: "read_file".into(), +- tool_call_id: "tc1".into(), +- arguments: serde_json::json!({"path": "src/main.rs"}), +- }), ++ &agent_event( ++ "code", ++ AgentEvent::ToolCallStarted { ++ tool_name: "read_file".into(), ++ tool_call_id: "tc1".into(), ++ arguments: serde_json::json!({"path": "src/main.rs"}), ++ }, ++ ), + started_ts, + None, + )) + .unwrap(); + let tool_completed = serde_json::to_string(&to_run_event_at( + &fixtures::RUN_1, +- &agent_event("code", AgentEvent::ToolCallCompleted { +- tool_name: "read_file".into(), +- tool_call_id: "tc1".into(), +- output: serde_json::json!({"ok": true}), +- is_error: false, +- }), ++ &agent_event( ++ "code", ++ AgentEvent::ToolCallCompleted { ++ tool_name: "read_file".into(), ++ tool_call_id: "tc1".into(), ++ output: serde_json::json!({"ok": true}), ++ is_error: false, ++ }, ++ ), + completed_ts, + None, + )) +diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs +index c522efac..0f25fbf0 100644 +--- a/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs ++++ b/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs +@@ -12,14 +12,14 @@ enum RendererInner { + } + + pub(super) struct ProgressRenderer { +- inner: RendererInner, ++ inner: RendererInner, + styles: Styles, + } + + impl ProgressRenderer { + pub(super) fn new_tty() -> Self { + Self { +- inner: RendererInner::Tty { ++ inner: RendererInner::Tty { + multi: MultiProgress::new(), + }, + styles: Styles::new(console::colors_enabled_stderr()), +@@ -28,7 +28,7 @@ impl ProgressRenderer { + + pub(super) fn new_plain(out: Box, colors: bool) -> Self { + Self { +- inner: RendererInner::Plain { ++ inner: RendererInner::Plain { + out: Mutex::new(out), + }, + styles: Styles::new(colors), +diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs +index e0212478..0fbb7eb5 100644 +--- a/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs ++++ b/lib/crates/fabro-cli/src/commands/run/run_progress/stage_display.rs +@@ -24,18 +24,18 @@ pub(super) enum ToolCallStatus { + pub(super) struct ToolCallEntry { + pub(super) display_name: String, + pub(super) tool_call_id: String, +- pub(super) status: ToolCallStatus, +- pub(super) bar: ProgressBar, +- pub(super) is_branch: bool, +- pub(super) started_at: Option>, ++ pub(super) status: ToolCallStatus, ++ pub(super) bar: ProgressBar, ++ pub(super) is_branch: bool, ++ pub(super) started_at: Option>, + } + + #[derive(Debug)] + pub(super) struct ActiveStage { +- pub(super) display_name: String, +- pub(super) has_model: bool, +- pub(super) spinner: ProgressBar, +- pub(super) tool_calls: VecDeque, ++ pub(super) display_name: String, ++ pub(super) has_model: bool, ++ pub(super) spinner: ProgressBar, ++ pub(super) tool_calls: VecDeque, + pub(super) compaction_bar: Option, + } + +@@ -48,12 +48,12 @@ impl ActiveStage { + } + + pub(super) struct StageDisplay { +- verbose: bool, +- pub(super) active_stages: HashMap, +- pub(super) stage_counts: HashMap, ++ verbose: bool, ++ pub(super) active_stages: HashMap, ++ pub(super) stage_counts: HashMap, + pub(super) parallel_parent: Option, +- any_stage_started: bool, +- working_directory: Option, ++ any_stage_started: bool, ++ working_directory: Option, + } + + impl StageDisplay { +@@ -117,13 +117,16 @@ impl StageDisplay { + if renderer.is_tty() { + bar.enable_steady_tick(Duration::from_millis(100)); + } +- self.active_stages.insert(node_id.to_string(), ActiveStage { +- display_name, +- has_model: false, +- spinner: bar, +- tool_calls: VecDeque::new(), +- compaction_bar: None, +- }); ++ self.active_stages.insert( ++ node_id.to_string(), ++ ActiveStage { ++ display_name, ++ has_model: false, ++ spinner: bar, ++ tool_calls: VecDeque::new(), ++ compaction_bar: None, ++ }, ++ ); + } + + pub(super) fn on_stage_completed( +diff --git a/lib/crates/fabro-cli/src/commands/run/runner.rs b/lib/crates/fabro-cli/src/commands/run/runner.rs +index 88a7a1f9..f93ef752 100644 +--- a/lib/crates/fabro-cli/src/commands/run/runner.rs ++++ b/lib/crates/fabro-cli/src/commands/run/runner.rs +@@ -247,8 +247,8 @@ fn build_artifact_uploader( + } + + struct HttpArtifactUploader { +- run_id: RunId, +- client: server_client::ServerStoreClient, ++ run_id: RunId, ++ client: server_client::ServerStoreClient, + bearer_token: String, + } + +@@ -313,7 +313,7 @@ impl StageArtifactUploader for MissingArtifactUploadTokenUploader { + struct HttpRunStore { + run_id: RunId, + client: server_client::ServerStoreClient, +- state: Arc>, ++ state: Arc>, + events: Arc>>>, + } + +@@ -637,12 +637,12 @@ mod tests { + ); + assert_eq!( + worker_title_phase_for_event(&EventBody::InterviewStarted(InterviewStartedProps { +- question_id: "q-1".to_string(), +- question: "Approve?".to_string(), +- stage: "gate".to_string(), +- question_type: "yes_no".to_string(), +- options: Vec::new(), +- allow_freeform: false, ++ question_id: "q-1".to_string(), ++ question: "Approve?".to_string(), ++ stage: "gate".to_string(), ++ question_type: "yes_no".to_string(), ++ options: Vec::new(), ++ allow_freeform: false, + timeout_seconds: None, + context_display: None, + })), +@@ -651,39 +651,39 @@ mod tests { + assert_eq!( + worker_title_phase_for_event(&EventBody::InterviewCompleted(InterviewCompletedProps { + question_id: "q-1".to_string(), +- question: "Approve?".to_string(), +- answer: "yes".to_string(), ++ question: "Approve?".to_string(), ++ answer: "yes".to_string(), + duration_ms: 10, + })), + Some(WorkerTitlePhase::Running) + ); + assert_eq!( + worker_title_phase_for_event(&EventBody::RunCompleted(RunCompletedProps { +- duration_ms: 10, +- artifact_count: 0, +- status: "success".to_string(), +- reason: None, +- total_usd_micros: None, ++ duration_ms: 10, ++ artifact_count: 0, ++ status: "success".to_string(), ++ reason: None, ++ total_usd_micros: None, + final_git_commit_sha: None, +- final_patch: None, +- billing: None, ++ final_patch: None, ++ billing: None, + })), + Some(WorkerTitlePhase::Succeeded) + ); + assert_eq!( + worker_title_phase_for_event(&EventBody::RunFailed(RunFailedProps { +- error: "cancelled".to_string(), +- duration_ms: 10, +- reason: Some(StatusReason::Cancelled), ++ error: "cancelled".to_string(), ++ duration_ms: 10, ++ reason: Some(StatusReason::Cancelled), + git_commit_sha: None, + })), + Some(WorkerTitlePhase::Cancelled) + ); + assert_eq!( + worker_title_phase_for_event(&EventBody::RunFailed(RunFailedProps { +- error: "boom".to_string(), +- duration_ms: 10, +- reason: Some(StatusReason::Terminated), ++ error: "boom".to_string(), ++ duration_ms: 10, ++ reason: Some(StatusReason::Terminated), + git_commit_sha: None, + })), + Some(WorkerTitlePhase::Failed) +@@ -815,7 +815,7 @@ mod tests { + "anthropic", + &serde_json::to_string(&AuthCredential { + provider: Provider::Anthropic, +- details: AuthDetails::ApiKey { ++ details: AuthDetails::ApiKey { + key: "vault-key".to_string(), + }, + }) +diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs +index 33bfaf18..09f73ed7 100644 +--- a/lib/crates/fabro-cli/src/commands/run/wait.rs ++++ b/lib/crates/fabro-cli/src/commands/run/wait.rs +@@ -170,22 +170,22 @@ mod tests { + fn json_output_succeeded_with_conclusion() { + let run_id = fixtures::RUN_1; + let conclusion = Conclusion { +- timestamp: chrono::Utc::now(), +- status: StageStatus::Success, +- duration_ms: 12345, +- failure_reason: None, ++ timestamp: chrono::Utc::now(), ++ status: StageStatus::Success, ++ duration_ms: 12345, ++ failure_reason: None, + final_git_commit_sha: None, +- stages: vec![], +- billing: Some(BilledTokenCounts { +- input_tokens: 0, +- output_tokens: 0, +- total_tokens: 0, +- reasoning_tokens: 0, +- cache_read_tokens: 0, ++ stages: vec![], ++ billing: Some(BilledTokenCounts { ++ input_tokens: 0, ++ output_tokens: 0, ++ total_tokens: 0, ++ reasoning_tokens: 0, ++ cache_read_tokens: 0, + cache_write_tokens: 0, +- total_usd_micros: Some(420_000), ++ total_usd_micros: Some(420_000), + }), +- total_retries: 0, ++ total_retries: 0, + }; + let json = build_json_output(RunStatus::Succeeded, &run_id, Some(&conclusion)); + assert_eq!(json["run_id"], run_id.to_string()); +@@ -214,14 +214,14 @@ mod tests { + fn json_output_no_cost_when_none() { + let run_id = fixtures::RUN_4; + let conclusion = Conclusion { +- timestamp: chrono::Utc::now(), +- status: StageStatus::Fail, +- duration_ms: 500, +- failure_reason: Some("error".into()), ++ timestamp: chrono::Utc::now(), ++ status: StageStatus::Fail, ++ duration_ms: 500, ++ failure_reason: Some("error".into()), + final_git_commit_sha: None, +- stages: vec![], +- billing: None, +- total_retries: 0, ++ stages: vec![], ++ billing: None, ++ total_retries: 0, + }; + let json = build_json_output(RunStatus::Failed, &run_id, Some(&conclusion)); + assert!(json.get("total_usd_micros").is_none()); +@@ -233,22 +233,22 @@ mod tests { + let styles = no_color_styles(); + let run_id = fixtures::RUN_5; + let conclusion = Conclusion { +- timestamp: chrono::Utc::now(), +- status: StageStatus::Success, +- duration_ms: 8000, +- failure_reason: None, ++ timestamp: chrono::Utc::now(), ++ status: StageStatus::Success, ++ duration_ms: 8000, ++ failure_reason: None, + final_git_commit_sha: None, +- stages: vec![], +- billing: Some(BilledTokenCounts { +- input_tokens: 0, +- output_tokens: 0, +- total_tokens: 0, +- reasoning_tokens: 0, +- cache_read_tokens: 0, ++ stages: vec![], ++ billing: Some(BilledTokenCounts { ++ input_tokens: 0, ++ output_tokens: 0, ++ total_tokens: 0, ++ reasoning_tokens: 0, ++ cache_read_tokens: 0, + cache_write_tokens: 0, +- total_usd_micros: Some(150_000), ++ total_usd_micros: Some(150_000), + }), +- total_retries: 0, ++ total_retries: 0, + }; + // Just verify no panic; actual stderr output is hard to capture + print_human_output( +diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs +index a014e9d5..4aeb0f24 100644 +--- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs ++++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs +@@ -12,13 +12,13 @@ use crate::server_runs::{ServerRunSummaryInfo, ServerSummaryLookup}; + + #[derive(Debug, Serialize)] + pub(crate) struct InspectOutput { +- pub run_id: String, +- pub status: RunStatus, +- pub run_record: Option, ++ pub run_id: String, ++ pub status: RunStatus, ++ pub run_record: Option, + pub start_record: Option, +- pub conclusion: Option, +- pub checkpoint: Option, +- pub sandbox: Option, ++ pub conclusion: Option, ++ pub checkpoint: Option, ++ pub sandbox: Option, + } + + pub(crate) async fn run( +@@ -40,24 +40,24 @@ pub(crate) async fn run( + + fn inspect_run_state(run: &ServerRunSummaryInfo, state: RunProjection) -> InspectOutput { + InspectOutput { +- run_id: run.run_id().to_string(), +- status: state ++ run_id: run.run_id().to_string(), ++ status: state + .status + .as_ref() + .map_or(run.status(), |record| record.status), +- run_record: state ++ run_record: state + .run + .and_then(|record| serde_json::to_value(record).ok()), + start_record: state + .start + .and_then(|record| serde_json::to_value(record).ok()), +- conclusion: state ++ conclusion: state + .conclusion + .and_then(|record| serde_json::to_value(record).ok()), +- checkpoint: state ++ checkpoint: state + .checkpoint + .and_then(|record| serde_json::to_value(record).ok()), +- sandbox: state ++ sandbox: state + .sandbox + .and_then(|record| serde_json::to_value(record).ok()), + } +diff --git a/lib/crates/fabro-cli/src/commands/server/foreground.rs b/lib/crates/fabro-cli/src/commands/server/foreground.rs +index 390dc4a3..e3d8a3d5 100644 +--- a/lib/crates/fabro-cli/src/commands/server/foreground.rs ++++ b/lib/crates/fabro-cli/src/commands/server/foreground.rs +@@ -52,13 +52,16 @@ pub(crate) async fn execute( + styles, + storage_dir, + move |resolved_bind| { +- record::write_server_record(&record_path, &record::ServerRecord { +- pid, +- bind: resolved_bind.clone(), +- log_path: log_path.clone(), +- dev_token_path: dev_token_path.clone(), +- started_at: Utc::now(), +- }) ++ record::write_server_record( ++ &record_path, ++ &record::ServerRecord { ++ pid, ++ bind: resolved_bind.clone(), ++ log_path: log_path.clone(), ++ dev_token_path: dev_token_path.clone(), ++ started_at: Utc::now(), ++ }, ++ ) + }, + )) + .await +diff --git a/lib/crates/fabro-cli/src/commands/server/record.rs b/lib/crates/fabro-cli/src/commands/server/record.rs +index c98b886f..3de73718 100644 +--- a/lib/crates/fabro-cli/src/commands/server/record.rs ++++ b/lib/crates/fabro-cli/src/commands/server/record.rs +@@ -9,17 +9,17 @@ use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub(crate) struct ServerRecord { +- pub pid: u32, +- pub bind: Bind, +- pub log_path: PathBuf, ++ pub pid: u32, ++ pub bind: Bind, ++ pub log_path: PathBuf, + #[serde(skip_serializing_if = "Option::is_none")] + pub dev_token_path: Option, +- pub started_at: DateTime, ++ pub started_at: DateTime, + } + + #[derive(Debug, Clone)] + pub(crate) struct ActiveServerRecord { +- pub record: ServerRecord, ++ pub record: ServerRecord, + pub record_path: PathBuf, + } + +diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs +index ed723fce..d4358305 100644 +--- a/lib/crates/fabro-cli/src/commands/server/start.rs ++++ b/lib/crates/fabro-cli/src/commands/server/start.rs +@@ -278,13 +278,16 @@ async fn execute_foreground( + Some(storage_dir), + move |resolved_bind| { + print_dev_token(printer, &home, &token); +- record::write_server_record(&record_path, &record::ServerRecord { +- pid, +- bind: resolved_bind.clone(), +- log_path: log_path.clone(), +- dev_token_path: Some(home.dev_token_path()), +- started_at: Utc::now(), +- }) ++ record::write_server_record( ++ &record_path, ++ &record::ServerRecord { ++ pid, ++ bind: resolved_bind.clone(), ++ log_path: log_path.clone(), ++ dev_token_path: Some(home.dev_token_path()), ++ started_at: Utc::now(), ++ }, ++ ) + }, + )) + .await +diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs +index 67764a7d..8700215e 100644 +--- a/lib/crates/fabro-cli/src/commands/store/dump.rs ++++ b/lib/crates/fabro-cli/src/commands/store/dump.rs +@@ -90,9 +90,9 @@ fn finalize_export( + } + + struct DumpArtifact { +- stage_id: StageId, ++ stage_id: StageId, + relative_path: String, +- data: Vec, ++ data: Vec, + } + + trait DumpDataSource { +@@ -105,9 +105,9 @@ trait DumpDataSource { + + #[cfg(test)] + struct LocalDumpSource<'a> { +- run_store: &'a RunDatabase, ++ run_store: &'a RunDatabase, + artifact_store: &'a ArtifactStore, +- run_id: RunId, ++ run_id: RunId, + } + + #[cfg(test)] +@@ -148,9 +148,9 @@ impl DumpDataSource for LocalDumpSource<'_> { + ) + })?; + artifacts.push(DumpArtifact { +- stage_id: asset.node, ++ stage_id: asset.node, + relative_path: asset.filename, +- data: data.to_vec(), ++ data: data.to_vec(), + }); + } + Ok(artifacts) +@@ -371,52 +371,49 @@ mod tests { + + fn sample_status() -> RunStatusRecord { + RunStatusRecord { +- status: RunStatus::Running, +- reason: Some(StatusReason::SandboxInitializing), ++ status: RunStatus::Running, ++ reason: Some(StatusReason::SandboxInitializing), + updated_at: dt("2026-03-27T12:05:00Z"), + } + } + + fn sample_checkpoint(current_node: &str, visit: u32) -> Checkpoint { + Checkpoint { +- timestamp: dt("2026-03-27T12:10:00Z"), +- current_node: current_node.to_string(), +- completed_nodes: vec!["plan".to_string()], +- node_retries: HashMap::from([( +- current_node.to_string(), +- visit.saturating_sub(1), +- )]), +- context_values: HashMap::from([( ++ timestamp: dt("2026-03-27T12:10:00Z"), ++ current_node: current_node.to_string(), ++ completed_nodes: vec!["plan".to_string()], ++ node_retries: HashMap::from([(current_node.to_string(), visit.saturating_sub(1))]), ++ context_values: HashMap::from([( + "artifact".to_string(), + serde_json::json!({"kind": "summary"}), + )]), +- node_outcomes: HashMap::new(), +- next_node_id: Some("review".to_string()), +- git_commit_sha: Some("def456".to_string()), +- loop_failure_signatures: HashMap::new(), ++ node_outcomes: HashMap::new(), ++ next_node_id: Some("review".to_string()), ++ git_commit_sha: Some("def456".to_string()), ++ loop_failure_signatures: HashMap::new(), + restart_failure_signatures: HashMap::new(), +- node_visits: HashMap::from([(current_node.to_string(), visit as usize)]), ++ node_visits: HashMap::from([(current_node.to_string(), visit as usize)]), + } + } + + fn sample_conclusion() -> Conclusion { + Conclusion { +- timestamp: dt("2026-03-27T12:15:00Z"), +- status: StageStatus::Success, +- duration_ms: 3210, +- failure_reason: None, ++ timestamp: dt("2026-03-27T12:15:00Z"), ++ status: StageStatus::Success, ++ duration_ms: 3210, ++ failure_reason: None, + final_git_commit_sha: Some("feedbeef".to_string()), +- stages: Vec::new(), +- billing: Some(BilledTokenCounts { +- input_tokens: 10, +- output_tokens: 20, +- total_tokens: 150, +- reasoning_tokens: 50, +- cache_read_tokens: 30, ++ stages: Vec::new(), ++ billing: Some(BilledTokenCounts { ++ input_tokens: 10, ++ output_tokens: 20, ++ total_tokens: 150, ++ reasoning_tokens: 50, ++ cache_read_tokens: 30, + cache_write_tokens: 40, +- total_usd_micros: Some(1_250_000), ++ total_usd_micros: Some(1_250_000), + }), +- total_retries: 2, ++ total_retries: 2, + } + } + +@@ -429,12 +426,12 @@ mod tests { + smoothness: None, + stages: Vec::new(), + stats: AggregateStats { +- total_duration_ms: 3210, ++ total_duration_ms: 3210, + total_billing_usd_micros: Some(1_250_000), +- total_retries: 2, +- files_touched: vec!["src/lib.rs".to_string()], +- stages_completed: 3, +- stages_failed: 0, ++ total_retries: 2, ++ files_touched: vec!["src/lib.rs".to_string()], ++ stages_completed: 3, ++ stages_failed: 0, + }, + intent: Some("ship the fix".to_string()), + outcome: Some("done".to_string()), +@@ -446,11 +443,11 @@ mod tests { + + fn sample_sandbox() -> SandboxRecord { + SandboxRecord { +- provider: "local".to_string(), +- working_directory: "/tmp/night-sky".to_string(), +- identifier: Some("sandbox-1".to_string()), ++ provider: "local".to_string(), ++ working_directory: "/tmp/night-sky".to_string(), ++ identifier: Some("sandbox-1".to_string()), + host_working_directory: Some("/tmp/night-sky".to_string()), +- container_mount_point: None, ++ container_mount_point: None, + } + } + +@@ -487,171 +484,223 @@ mod tests { + ); + + let node = StageId::new("code", 2); +- append_event(&run, &run_id, &Event::RunCreated { +- run_id, +- settings: serde_json::to_value(&run_record.settings).unwrap(), +- graph: serde_json::to_value(&run_record.graph).unwrap(), +- workflow_source: Some("digraph night_sky {}".to_string()), +- workflow_config: None, +- labels: run_record.labels.clone().into_iter().collect(), +- run_dir: "/tmp/night-sky-run".to_string(), +- working_directory: run_record.working_directory.display().to_string(), +- host_repo_path: run_record.host_repo_path.clone(), +- repo_origin_url: run_record.repo_origin_url.clone(), +- base_branch: run_record.base_branch.clone(), +- workflow_slug: run_record.workflow_slug.clone(), +- db_prefix: None, +- provenance: run_record.provenance.clone(), +- manifest_blob: None, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::RunCreated { ++ run_id, ++ settings: serde_json::to_value(&run_record.settings).unwrap(), ++ graph: serde_json::to_value(&run_record.graph).unwrap(), ++ workflow_source: Some("digraph night_sky {}".to_string()), ++ workflow_config: None, ++ labels: run_record.labels.clone().into_iter().collect(), ++ run_dir: "/tmp/night-sky-run".to_string(), ++ working_directory: run_record.working_directory.display().to_string(), ++ host_repo_path: run_record.host_repo_path.clone(), ++ repo_origin_url: run_record.repo_origin_url.clone(), ++ base_branch: run_record.base_branch.clone(), ++ workflow_slug: run_record.workflow_slug.clone(), ++ db_prefix: None, ++ provenance: run_record.provenance.clone(), ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::WorkflowRunStarted { +- name: "night-sky".to_string(), +- run_id, +- base_branch: run_record.base_branch.clone(), +- base_sha: start_record.base_sha.clone(), +- run_branch: start_record.run_branch.clone(), +- worktree_dir: None, +- goal: Some("map the constellations".to_string()), +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::WorkflowRunStarted { ++ name: "night-sky".to_string(), ++ run_id, ++ base_branch: run_record.base_branch.clone(), ++ base_sha: start_record.base_sha.clone(), ++ run_branch: start_record.run_branch.clone(), ++ worktree_dir: None, ++ goal: Some("map the constellations".to_string()), ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::RunRunning { +- reason: status_record.reason, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::RunRunning { ++ reason: status_record.reason, ++ }, ++ ) + .await + .unwrap(); + for checkpoint in [&first_checkpoint, &second_checkpoint] { +- append_event(&run, &run_id, &Event::CheckpointCompleted { +- node_id: checkpoint.current_node.clone(), +- status: "success".to_string(), +- current_node: checkpoint.current_node.clone(), +- completed_nodes: checkpoint.completed_nodes.clone(), +- node_retries: checkpoint.node_retries.clone().into_iter().collect(), +- context_values: checkpoint.context_values.clone().into_iter().collect(), +- node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), +- next_node_id: checkpoint.next_node_id.clone(), +- git_commit_sha: checkpoint.git_commit_sha.clone(), +- loop_failure_signatures: checkpoint +- .loop_failure_signatures +- .clone() +- .into_iter() +- .map(|(signature, count)| (signature.to_string(), count)) +- .collect(), +- restart_failure_signatures: checkpoint +- .restart_failure_signatures +- .clone() +- .into_iter() +- .map(|(signature, count)| (signature.to_string(), count)) +- .collect(), +- node_visits: checkpoint.node_visits.clone().into_iter().collect(), +- diff: None, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::CheckpointCompleted { ++ node_id: checkpoint.current_node.clone(), ++ status: "success".to_string(), ++ current_node: checkpoint.current_node.clone(), ++ completed_nodes: checkpoint.completed_nodes.clone(), ++ node_retries: checkpoint.node_retries.clone().into_iter().collect(), ++ context_values: checkpoint.context_values.clone().into_iter().collect(), ++ node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), ++ next_node_id: checkpoint.next_node_id.clone(), ++ git_commit_sha: checkpoint.git_commit_sha.clone(), ++ loop_failure_signatures: checkpoint ++ .loop_failure_signatures ++ .clone() ++ .into_iter() ++ .map(|(signature, count)| (signature.to_string(), count)) ++ .collect(), ++ restart_failure_signatures: checkpoint ++ .restart_failure_signatures ++ .clone() ++ .into_iter() ++ .map(|(signature, count)| (signature.to_string(), count)) ++ .collect(), ++ node_visits: checkpoint.node_visits.clone().into_iter().collect(), ++ diff: None, ++ }, ++ ) + .await + .unwrap(); + } +- append_event(&run, &run_id, &Event::SandboxInitialized { +- working_directory: sandbox.working_directory.clone(), +- provider: sandbox.provider.clone(), +- identifier: sandbox.identifier.clone(), +- host_working_directory: sandbox.host_working_directory.clone(), +- container_mount_point: sandbox.container_mount_point.clone(), +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::SandboxInitialized { ++ working_directory: sandbox.working_directory.clone(), ++ provider: sandbox.provider.clone(), ++ identifier: sandbox.identifier.clone(), ++ host_working_directory: sandbox.host_working_directory.clone(), ++ container_mount_point: sandbox.container_mount_point.clone(), ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::Prompt { +- stage: "code".to_string(), +- visit: 2, +- text: "Plan the fix".to_string(), +- mode: None, +- provider: None, +- model: None, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::Prompt { ++ stage: "code".to_string(), ++ visit: 2, ++ text: "Plan the fix".to_string(), ++ mode: None, ++ provider: None, ++ model: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::PromptCompleted { +- node_id: "code".to_string(), +- response: "Implemented".to_string(), +- model: "gpt-5".to_string(), +- provider: "openai".to_string(), +- billing: None, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::PromptCompleted { ++ node_id: "code".to_string(), ++ response: "Implemented".to_string(), ++ model: "gpt-5".to_string(), ++ provider: "openai".to_string(), ++ billing: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::StageCompleted { +- node_id: "code".to_string(), +- name: "Code".to_string(), +- index: 1, +- duration_ms: 250, +- status: "partial_success".to_string(), +- preferred_label: None, +- suggested_next_ids: Vec::new(), +- billing: None, +- failure: None, +- notes: Some("captured output".to_string()), +- files_touched: Vec::new(), +- context_updates: None, +- jump_to_node: None, +- context_values: None, +- node_visits: Some(std::collections::BTreeMap::from([( +- "code".to_string(), +- 2usize, +- )])), +- loop_failure_signatures: None, +- restart_failure_signatures: None, +- response: Some("Implemented".to_string()), +- attempt: 1, +- max_attempts: 1, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::StageCompleted { ++ node_id: "code".to_string(), ++ name: "Code".to_string(), ++ index: 1, ++ duration_ms: 250, ++ status: "partial_success".to_string(), ++ preferred_label: None, ++ suggested_next_ids: Vec::new(), ++ billing: None, ++ failure: None, ++ notes: Some("captured output".to_string()), ++ files_touched: Vec::new(), ++ context_updates: None, ++ jump_to_node: None, ++ context_values: None, ++ node_visits: Some(std::collections::BTreeMap::from([( ++ "code".to_string(), ++ 2usize, ++ )])), ++ loop_failure_signatures: None, ++ restart_failure_signatures: None, ++ response: Some("Implemented".to_string()), ++ attempt: 1, ++ max_attempts: 1, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::CommandStarted { +- node_id: "code".to_string(), +- script: "echo hi".to_string(), +- command: "echo hi".to_string(), +- language: "sh".to_string(), +- timeout_ms: None, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::CommandStarted { ++ node_id: "code".to_string(), ++ script: "echo hi".to_string(), ++ command: "echo hi".to_string(), ++ language: "sh".to_string(), ++ timeout_ms: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::CommandCompleted { +- node_id: "code".to_string(), +- stdout: "stdout line".to_string(), +- stderr: String::new(), +- exit_code: Some(0), +- duration_ms: 100, +- timed_out: false, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::CommandCompleted { ++ node_id: "code".to_string(), ++ stdout: "stdout line".to_string(), ++ stderr: String::new(), ++ exit_code: Some(0), ++ duration_ms: 100, ++ timed_out: false, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::RetroStarted { +- prompt: Some("How did it go?".to_string()), +- provider: None, +- model: None, +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::RetroStarted { ++ prompt: Some("How did it go?".to_string()), ++ provider: None, ++ model: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::RetroCompleted { +- duration_ms: 50, +- response: Some("Smooth enough".to_string()), +- retro: Some(serde_json::to_value(&retro).unwrap()), +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::RetroCompleted { ++ duration_ms: 50, ++ response: Some("Smooth enough".to_string()), ++ retro: Some(serde_json::to_value(&retro).unwrap()), ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &run_id, &Event::WorkflowRunCompleted { +- duration_ms: conclusion.duration_ms, +- artifact_count: 0, +- status: "success".to_string(), +- reason: None, +- total_usd_micros: conclusion +- .billing +- .as_ref() +- .and_then(|billing| billing.total_usd_micros), +- final_git_commit_sha: conclusion.final_git_commit_sha.clone(), +- final_patch: None, +- billing: conclusion.billing.clone(), +- }) ++ append_event( ++ &run, ++ &run_id, ++ &Event::WorkflowRunCompleted { ++ duration_ms: conclusion.duration_ms, ++ artifact_count: 0, ++ status: "success".to_string(), ++ reason: None, ++ total_usd_micros: conclusion ++ .billing ++ .as_ref() ++ .and_then(|billing| billing.total_usd_micros), ++ final_git_commit_sha: conclusion.final_git_commit_sha.clone(), ++ final_patch: None, ++ billing: conclusion.billing.clone(), ++ }, ++ ) + .await + .unwrap(); + run.append_event( +diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs +index cb85235b..3ea34018 100644 +--- a/lib/crates/fabro-cli/src/commands/system/prune.rs ++++ b/lib/crates/fabro-cli/src/commands/system/prune.rs +@@ -24,12 +24,12 @@ pub(super) async fn prune_command( + .api() + .prune_runs() + .body(types::PruneRunsRequest { +- before: args.filter.before.clone(), +- dry_run: !args.yes, +- labels: parse_label_filters(&args.filter.label), ++ before: args.filter.before.clone(), ++ dry_run: !args.yes, ++ labels: parse_label_filters(&args.filter.label), + older_than: args.older_than.map(format_duration), +- orphans: args.filter.orphans, +- workflow: args.filter.workflow.clone(), ++ orphans: args.filter.orphans, ++ workflow: args.filter.workflow.clone(), + }) + .send() + .await +diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/crates/fabro-cli/src/commands/uninstall.rs +index c1d56b49..cbeec833 100644 +--- a/lib/crates/fabro-cli/src/commands/uninstall.rs ++++ b/lib/crates/fabro-cli/src/commands/uninstall.rs +@@ -18,13 +18,13 @@ use crate::user_config; + + #[derive(Debug, Serialize)] + struct Inventory { +- home_root: PathBuf, +- storage_dir: PathBuf, +- home_exists: bool, +- home_size: u64, +- server_running: bool, +- shell_configs: Vec, +- binary_path: Option, ++ home_root: PathBuf, ++ storage_dir: PathBuf, ++ home_exists: bool, ++ home_size: u64, ++ server_running: bool, ++ shell_configs: Vec, ++ binary_path: Option, + binary_is_managed: bool, + } + +@@ -221,12 +221,12 @@ fn print_preview(inventory: &Inventory, printer: Printer) { + + #[derive(Debug, Serialize)] + struct UninstallResult { +- status: &'static str, +- home_removed: bool, +- server_stopped: bool, ++ status: &'static str, ++ home_removed: bool, ++ server_stopped: bool, + shell_configs_cleaned: Vec, +- binary_removed: bool, +- binary_hint: Option, ++ binary_removed: bool, ++ binary_hint: Option, + } + + async fn execute_uninstall(inventory: &Inventory, json: bool, printer: Printer) -> Result<()> { +@@ -235,12 +235,12 @@ async fn execute_uninstall(inventory: &Inventory, json: bool, printer: Printer) + let bold = console::Style::new().bold(); + let mut critical_failure = false; + let mut result = UninstallResult { +- status: "completed", +- home_removed: false, +- server_stopped: false, ++ status: "completed", ++ home_removed: false, ++ server_stopped: false, + shell_configs_cleaned: Vec::new(), +- binary_removed: false, +- binary_hint: None, ++ binary_removed: false, ++ binary_hint: None, + }; + + // Unit 3a: Server stop +diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs +index 158b8590..97833d5d 100644 +--- a/lib/crates/fabro-cli/src/commands/upgrade.rs ++++ b/lib/crates/fabro-cli/src/commands/upgrade.rs +@@ -192,7 +192,7 @@ const LAST_CHECK_FILE: &str = "last_upgrade_check.json"; + + #[derive(serde::Serialize, serde::Deserialize)] + struct UpgradeCheckState { +- checked_at: u64, ++ checked_at: u64, + latest_version: String, + } + +@@ -411,7 +411,7 @@ async fn check_and_print_notice(printer: Printer) -> Result<()> { + .unwrap_or_default() + .as_secs(); + let state = UpgradeCheckState { +- checked_at: now, ++ checked_at: now, + latest_version: latest.to_string(), + }; + let _ = state.save(&state_path); +@@ -507,7 +507,7 @@ mod tests { + #[test] + fn upgrade_check_state_roundtrip() { + let state = UpgradeCheckState { +- checked_at: 1_710_000_000, ++ checked_at: 1_710_000_000, + latest_version: "0.5.0".to_string(), + }; + let json = serde_json::to_string(&state).unwrap(); +@@ -519,7 +519,7 @@ mod tests { + #[test] + fn upgrade_check_state_stale() { + let old = UpgradeCheckState { +- checked_at: 0, // epoch — definitely stale ++ checked_at: 0, // epoch — definitely stale + latest_version: "0.1.0".to_string(), + }; + assert!(old.is_stale()); +@@ -532,7 +532,7 @@ mod tests { + .unwrap() + .as_secs(); + let fresh = UpgradeCheckState { +- checked_at: now, ++ checked_at: now, + latest_version: "0.5.0".to_string(), + }; + assert!(!fresh.is_stale()); +@@ -543,7 +543,7 @@ mod tests { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("state.json"); + let state = UpgradeCheckState { +- checked_at: 1_710_000_000, ++ checked_at: 1_710_000_000, + latest_version: "0.5.0".to_string(), + }; + state.save(&path).unwrap(); +diff --git a/lib/crates/fabro-cli/src/commands/validate.rs b/lib/crates/fabro-cli/src/commands/validate.rs +index a530e547..c263f121 100644 +--- a/lib/crates/fabro-cli/src/commands/validate.rs ++++ b/lib/crates/fabro-cli/src/commands/validate.rs +@@ -21,12 +21,12 @@ pub(crate) async fn run( + ) -> anyhow::Result<()> { + let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?; + let built = build_run_manifest(ManifestBuildInput { +- workflow: args.workflow.clone(), +- cwd: ctx.cwd().to_path_buf(), +- args_layer: SettingsLayer::default(), +- args: None, +- run_id: None, +- user_layer: load_settings_user()?, ++ workflow: args.workflow.clone(), ++ cwd: ctx.cwd().to_path_buf(), ++ args_layer: SettingsLayer::default(), ++ args: None, ++ run_id: None, ++ user_layer: load_settings_user()?, + user_settings_path: Some(active_settings_path(None)), + })?; + let client = ctx.server().await?; +diff --git a/lib/crates/fabro-cli/src/commands/version.rs b/lib/crates/fabro-cli/src/commands/version.rs +index 6b90f976..e37cb6ec 100644 +--- a/lib/crates/fabro-cli/src/commands/version.rs ++++ b/lib/crates/fabro-cli/src/commands/version.rs +@@ -31,23 +31,23 @@ pub(crate) async fn version_command( + Ok(response) => { + let response = response.into_inner(); + ServerVersionInfo::Success { +- address: server_address, +- version: response.version, +- git_sha: response.git_sha, +- build_date: response.build_date, +- os: response.os, +- arch: response.arch, ++ address: server_address, ++ version: response.version, ++ git_sha: response.git_sha, ++ build_date: response.build_date, ++ os: response.os, ++ arch: response.arch, + uptime_secs: response.uptime_secs, + } + } + Err(err) => ServerVersionInfo::Error { + address: server_address, +- error: err.to_string(), ++ error: err.to_string(), + }, + }, + Err(err) => ServerVersionInfo::Error { + address: server_address, +- error: err.to_string(), ++ error: err.to_string(), + }, + }; + +@@ -61,36 +61,36 @@ pub(crate) async fn version_command( + } + + struct ClientVersionInfo { +- version: &'static str, +- git_sha: &'static str, ++ version: &'static str, ++ git_sha: &'static str, + build_date: &'static str, +- os: &'static str, +- arch: &'static str, ++ os: &'static str, ++ arch: &'static str, + } + + enum ServerVersionInfo { + Success { +- address: String, +- version: Option, +- git_sha: Option, +- build_date: Option, +- os: Option, +- arch: Option, ++ address: String, ++ version: Option, ++ git_sha: Option, ++ build_date: Option, ++ os: Option, ++ arch: Option, + uptime_secs: Option, + }, + Error { + address: String, +- error: String, ++ error: String, + }, + } + + fn client_info() -> ClientVersionInfo { + ClientVersionInfo { +- version: env!("CARGO_PKG_VERSION"), +- git_sha: env!("FABRO_GIT_SHA"), ++ version: env!("CARGO_PKG_VERSION"), ++ git_sha: env!("FABRO_GIT_SHA"), + build_date: env!("FABRO_BUILD_DATE"), +- os: std::env::consts::OS, +- arch: std::env::consts::ARCH, ++ os: std::env::consts::OS, ++ arch: std::env::consts::ARCH, + } + } + +diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs +index 1ec8d43b..45df1e33 100644 +--- a/lib/crates/fabro-cli/src/main.rs ++++ b/lib/crates/fabro-cli/src/main.rs +@@ -140,10 +140,13 @@ async fn main_inner() -> (String, Result<()>) { + Ok(settings) => settings, + Err(err) => return (command_name, Err(err)), + }; +- let combined_settings = combine_files(user_settings, SettingsLayer { +- cli: Some(cli_layer.clone()), +- ..SettingsLayer::default() +- }); ++ let combined_settings = combine_files( ++ user_settings, ++ SettingsLayer { ++ cli: Some(cli_layer.clone()), ++ ..SettingsLayer::default() ++ }, ++ ); + let cli_settings = match user_config::resolve_cli_settings(&combined_settings) { + Ok(cli_settings) => cli_settings, + Err(err) => return (command_name, Err(err)), +diff --git a/lib/crates/fabro-cli/src/manifest_builder.rs b/lib/crates/fabro-cli/src/manifest_builder.rs +index 4aefd300..c5d184ee 100644 +--- a/lib/crates/fabro-cli/src/manifest_builder.rs ++++ b/lib/crates/fabro-cli/src/manifest_builder.rs +@@ -19,14 +19,14 @@ use crate::args::{PreflightArgs, RunArgs}; + + #[derive(Debug)] + pub(crate) struct ManifestBuildInput { +- pub workflow: PathBuf, +- pub cwd: PathBuf, +- pub args_layer: SettingsLayer, +- pub args: Option, +- pub run_id: Option, ++ pub workflow: PathBuf, ++ pub cwd: PathBuf, ++ pub args_layer: SettingsLayer, ++ pub args: Option, ++ pub run_id: Option, + /// User-level settings layer. Production callers load via + /// `load_settings_user()`; tests pass `SettingsLayer::default()`. +- pub user_layer: SettingsLayer, ++ pub user_layer: SettingsLayer, + /// Path to the user settings file (for inclusion in + /// `RunManifest.configs`). `None` skips the user config entry. + pub user_settings_path: Option, +@@ -34,21 +34,21 @@ pub(crate) struct ManifestBuildInput { + + #[derive(Debug)] + pub(crate) struct BuiltManifest { +- pub manifest: types::RunManifest, ++ pub manifest: types::RunManifest, + pub target_path: PathBuf, + } + + struct CollectContext<'a> { +- cwd: &'a Path, +- workflows: HashMap, ++ cwd: &'a Path, ++ workflows: HashMap, + visited_workflows: HashSet, + } + + #[derive(Clone)] + struct WorkflowScanInput { + absolute_dot_path: PathBuf, +- logical_dot_path: PathBuf, +- source: String, ++ logical_dot_path: PathBuf, ++ source: String, + } + + pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result { +@@ -64,8 +64,8 @@ pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result Result Result Result Option { + let payload = types::ManifestArgs { +- auto_approve: args.auto_approve.then_some(true), +- dry_run: args.dry_run.then_some(true), +- label: args.label.clone(), +- model: args.model.clone(), +- no_retro: args.no_retro.then_some(true), ++ auto_approve: args.auto_approve.then_some(true), ++ dry_run: args.dry_run.then_some(true), ++ label: args.label.clone(), ++ model: args.model.clone(), ++ no_retro: args.no_retro.then_some(true), + preserve_sandbox: args.preserve_sandbox.then_some(true), +- provider: args.provider.clone(), +- sandbox: args ++ provider: args.provider.clone(), ++ sandbox: args + .sandbox + .map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string()), +- verbose: args.verbose.then_some(true), ++ verbose: args.verbose.then_some(true), + }; + (!manifest_args_is_empty(&payload)).then_some(payload) + } + + pub(crate) fn preflight_manifest_args(args: &PreflightArgs) -> Option { + let payload = types::ManifestArgs { +- auto_approve: None, +- dry_run: None, +- label: Vec::new(), +- model: args.model.clone(), +- no_retro: None, ++ auto_approve: None, ++ dry_run: None, ++ label: Vec::new(), ++ model: args.model.clone(), ++ no_retro: None, + preserve_sandbox: None, +- provider: args.provider.clone(), +- sandbox: args ++ provider: args.provider.clone(), ++ sandbox: args + .sandbox + .map(|provider| fabro_sandbox::SandboxProvider::from(provider).to_string()), +- verbose: args.verbose.then_some(true), ++ verbose: args.verbose.then_some(true), + }; + (!manifest_args_is_empty(&payload)).then_some(payload) + } +@@ -191,7 +191,7 @@ fn collect_workflow_entry( + .with_context(|| format!("Failed to read {}", resolution.dot_path.display()))?; + let config = if let Some(workflow_toml_path) = resolution.workflow_toml_path.as_ref() { + Some(types::ManifestWorkflowConfig { +- path: logical_path_string(&to_logical_path(workflow_toml_path, context.cwd)?), ++ path: logical_path_string(&to_logical_path(workflow_toml_path, context.cwd)?), + source: std::fs::read_to_string(workflow_toml_path) + .with_context(|| format!("Failed to read {}", workflow_toml_path.display()))?, + }) +@@ -211,13 +211,14 @@ fn collect_workflow_entry( + } + collect_workflow_files(context, &scan, &mut files, &mut visited_imports)?; + +- context +- .workflows +- .insert(logical_dot_key, types::ManifestWorkflow { ++ context.workflows.insert( ++ logical_dot_key, ++ types::ManifestWorkflow { + config, + files, + source, +- }); ++ }, ++ ); + + Ok(()) + } +@@ -288,8 +289,8 @@ fn collect_workflow_files( + })?; + let imported_scan = WorkflowScanInput { + absolute_dot_path: imported.absolute_path, +- logical_dot_path: imported.logical_path, +- source: imported_source, ++ logical_dot_path: imported.logical_path, ++ source: imported_source, + }; + collect_workflow_files(context, &imported_scan, files, visited_imports)?; + } +@@ -347,7 +348,7 @@ fn collect_workflow_config_files( + + struct BundledFile { + absolute_path: PathBuf, +- logical_path: PathBuf, ++ logical_path: PathBuf, + } + + fn collect_bundled_file( +@@ -365,14 +366,17 @@ fn collect_bundled_file( + if !files.contains_key(&key) { + let content = std::fs::read_to_string(&absolute_path) + .with_context(|| format!("Failed to read {}", absolute_path.display()))?; +- files.insert(key.clone(), types::ManifestFileEntry { +- content, +- ref_: types::ManifestFileRef { +- from: from.map(|value| logical_path_string(&value)), +- original: reference.to_string(), +- type_: ref_type, ++ files.insert( ++ key.clone(), ++ types::ManifestFileEntry { ++ content, ++ ref_: types::ManifestFileRef { ++ from: from.map(|value| logical_path_string(&value)), ++ original: reference.to_string(), ++ type_: ref_type, ++ }, + }, +- }); ++ ); + } + + Ok(BundledFile { +@@ -421,16 +425,16 @@ fn resolve_manifest_goal( + ) + .ok_or_else(|| anyhow!("unsupported manifest goal reference: {reference}"))?; + return Ok(Some(types::ManifestGoal { +- path: Some(reference.to_string()), +- text: std::fs::read_to_string(&goal_path) ++ path: Some(reference.to_string()), ++ text: std::fs::read_to_string(&goal_path) + .with_context(|| format!("Failed to read {}", goal_path.display()))?, + type_: types::ManifestGoalType::Graph, + })); + } + + Ok(Some(types::ManifestGoal { +- path: None, +- text: goal.to_string(), ++ path: None, ++ text: goal.to_string(), + type_: types::ManifestGoalType::Graph, + })) + } +@@ -441,13 +445,13 @@ fn resolve_manifest_goal( + fn resolved_goal_to_manifest(resolved: ResolvedRunGoal) -> types::ManifestGoal { + match resolved.source { + ResolvedGoalSource::Inline => types::ManifestGoal { +- path: None, +- text: resolved.text, ++ path: None, ++ text: resolved.text, + type_: types::ManifestGoalType::Value, + }, + ResolvedGoalSource::File { path } => types::ManifestGoal { +- path: Some(path.to_string_lossy().into_owned()), +- text: resolved.text, ++ path: Some(path.to_string_lossy().into_owned()), ++ text: resolved.text, + type_: types::ManifestGoalType::File, + }, + } +@@ -600,12 +604,12 @@ mod tests { + .unwrap(); + + let built = build_run_manifest(ManifestBuildInput { +- workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), +- cwd: project.to_path_buf(), +- args_layer: SettingsLayer::default(), +- args: None, +- run_id: None, +- user_layer: SettingsLayer::default(), ++ workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), ++ cwd: project.to_path_buf(), ++ args_layer: SettingsLayer::default(), ++ args: None, ++ run_id: None, ++ user_layer: SettingsLayer::default(), + user_settings_path: None, + }) + .unwrap(); +@@ -680,12 +684,12 @@ file = "prompts/goal.md" + .unwrap(); + + let built = build_run_manifest(ManifestBuildInput { +- workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), +- cwd: project.to_path_buf(), +- args_layer: SettingsLayer::default(), +- args: None, +- run_id: None, +- user_layer: SettingsLayer::default(), ++ workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), ++ cwd: project.to_path_buf(), ++ args_layer: SettingsLayer::default(), ++ args: None, ++ run_id: None, ++ user_layer: SettingsLayer::default(), + user_settings_path: None, + }) + .unwrap(); +@@ -733,12 +737,12 @@ file = "prompts/goal.md" + .unwrap(); + + let built = build_run_manifest(ManifestBuildInput { +- workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), +- cwd: project.to_path_buf(), +- args_layer: SettingsLayer::default(), +- args: None, +- run_id: None, +- user_layer: SettingsLayer::default(), ++ workflow: PathBuf::from(".fabro/workflows/demo/workflow.toml"), ++ cwd: project.to_path_buf(), ++ args_layer: SettingsLayer::default(), ++ args: None, ++ run_id: None, ++ user_layer: SettingsLayer::default(), + user_settings_path: None, + }) + .unwrap(); +diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/crates/fabro-cli/src/server_client.rs +index eae036e4..bed9835a 100644 +--- a/lib/crates/fabro-cli/src/server_client.rs ++++ b/lib/crates/fabro-cli/src/server_client.rs +@@ -31,20 +31,20 @@ use crate::{sse, user_config}; + + #[derive(Clone)] + pub(crate) struct ServerStoreClient { +- client: fabro_api::Client, ++ client: fabro_api::Client, + http_client: fabro_http::HttpClient, +- base_url: String, ++ base_url: String, + } + + #[derive(Debug, Clone)] + struct LocalServerRuntime { + active_config_path: PathBuf, +- storage_dir: PathBuf, ++ storage_dir: PathBuf, + } + + pub(crate) struct RunAttachEventStream { +- stream: progenitor_client::ByteStream, +- pending_bytes: Vec, ++ stream: progenitor_client::ByteStream, ++ pending_bytes: Vec, + buffered_events: VecDeque, + } + +@@ -109,7 +109,7 @@ pub(crate) async fn connect_server_with_settings( + let target = user_config::resolve_server_target(args, settings)?; + let runtime = LocalServerRuntime { + active_config_path: base_config_path.to_path_buf(), +- storage_dir: user_config::storage_dir(settings)?, ++ storage_dir: user_config::storage_dir(settings)?, + }; + connect_target_api_client_bundle(&target, &runtime).await + } +@@ -436,14 +436,14 @@ struct ArtifactBatchUploadManifest { + + #[derive(Debug, Serialize)] + struct ArtifactBatchUploadEntry { +- part: String, +- path: String, ++ part: String, ++ path: String, + #[serde(skip_serializing_if = "Option::is_none")] +- sha256: Option, ++ sha256: Option, + #[serde(skip_serializing_if = "Option::is_none")] + expected_bytes: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- content_type: Option, ++ content_type: Option, + } + + impl ServerStoreClient { +@@ -887,11 +887,11 @@ impl ServerStoreClient { + .len(); + + manifest_entries.push(ArtifactBatchUploadEntry { +- part: part_name.clone(), +- path: artifact.path.clone(), +- sha256: Some(artifact.content_sha256.clone()), ++ part: part_name.clone(), ++ path: artifact.path.clone(), ++ sha256: Some(artifact.content_sha256.clone()), + expected_bytes: Some(artifact.bytes), +- content_type: Some(artifact.mime.clone()), ++ content_type: Some(artifact.mime.clone()), + }); + + file_parts.push(( +@@ -1168,13 +1168,16 @@ mod tests { + let record_path = fabro_config::Storage::new(storage.path()) + .server_state() + .record_path(); +- record::write_server_record(&record_path, &record::ServerRecord { +- pid: std::process::id(), +- bind: Bind::Unix(temp_home.path().join("fabro.sock")), +- log_path: storage.path().join("server.log"), +- dev_token_path: Some(token_path), +- started_at: chrono::Utc::now(), +- }) ++ record::write_server_record( ++ &record_path, ++ &record::ServerRecord { ++ pid: std::process::id(), ++ bind: Bind::Unix(temp_home.path().join("fabro.sock")), ++ log_path: storage.path().join("server.log"), ++ dev_token_path: Some(token_path), ++ started_at: chrono::Utc::now(), ++ }, ++ ) + .unwrap(); + + std::env::remove_var("FABRO_DEV_TOKEN"); +diff --git a/lib/crates/fabro-cli/src/server_runs.rs b/lib/crates/fabro-cli/src/server_runs.rs +index 8020f788..f3ef05b7 100644 +--- a/lib/crates/fabro-cli/src/server_runs.rs ++++ b/lib/crates/fabro-cli/src/server_runs.rs +@@ -11,9 +11,9 @@ use fabro_workflow::run_lookup::{RunInfo, resolve_run_from_summaries, scratch_ba + use crate::server_client::{self, ServerStoreClient}; + + pub(crate) struct ServerRunLookup { +- client: ServerStoreClient, ++ client: ServerStoreClient, + scratch_base: PathBuf, +- summaries: Vec, ++ summaries: Vec, + } + + impl ServerRunLookup { +@@ -105,7 +105,7 @@ impl ServerRunSummaryInfo { + + pub(crate) struct ServerSummaryLookup { + client: Arc, +- runs: Vec, ++ runs: Vec, + } + + impl ServerSummaryLookup { +diff --git a/lib/crates/fabro-cli/src/shared/provider_auth.rs b/lib/crates/fabro-cli/src/shared/provider_auth.rs +index 2c356a1f..67c4e05a 100644 +--- a/lib/crates/fabro-cli/src/shared/provider_auth.rs ++++ b/lib/crates/fabro-cli/src/shared/provider_auth.rs +@@ -71,7 +71,7 @@ pub(crate) enum ApiKeySource { + pub(crate) async fn validate_api_key(provider: Provider, api_key: &str) -> Result<(), String> { + let auth_header = if provider == Provider::Anthropic { + ApiKeyHeader::Custom { +- name: "x-api-key".to_string(), ++ name: "x-api-key".to_string(), + value: api_key.to_string(), + } + } else { +diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs +index 4074284d..45869a19 100644 +--- a/lib/crates/fabro-cli/src/user_config.rs ++++ b/lib/crates/fabro-cli/src/user_config.rs +@@ -12,8 +12,8 @@ use tracing::debug; + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + pub(crate) struct ClientTlsSettings { + pub cert: PathBuf, +- pub key: PathBuf, +- pub ca: PathBuf, ++ pub key: PathBuf, ++ pub ca: PathBuf, + } + + use crate::args::ServerTargetArgs; +@@ -85,7 +85,7 @@ pub(crate) fn apply_storage_dir_override( + pub(crate) enum ServerTarget { + HttpUrl { + api_url: String, +- tls: Option, ++ tls: Option, + }, + UnixSocket(PathBuf), + } +@@ -99,8 +99,8 @@ fn cli_target_from_settings(settings: &CliSettings) -> Option<(String, Option { + let tls_settings = tls.as_ref().map(|tls| ClientTlsSettings { + cert: PathBuf::from(tls.cert.as_source()), +- key: PathBuf::from(tls.key.as_source()), +- ca: PathBuf::from(tls.ca.as_source()), ++ key: PathBuf::from(tls.key.as_source()), ++ ca: PathBuf::from(tls.ca.as_source()), + }); + Some((url.as_source(), tls_settings)) + } +@@ -268,7 +268,7 @@ mod tests { + .unwrap(), + Some(ServerTarget::HttpUrl { + api_url: "https://cli.example.com".to_string(), +- tls: None, ++ tls: None, + }) + ); + } +@@ -314,7 +314,7 @@ url = "https://config.example.com" + resolve_server_target(&server_target_args(None), &settings).unwrap(), + ServerTarget::HttpUrl { + api_url: "https://config.example.com".to_string(), +- tls: None, ++ tls: None, + } + ); + } +@@ -338,7 +338,7 @@ url = "https://config.example.com" + .unwrap(), + ServerTarget::HttpUrl { + api_url: "https://cli.example.com".to_string(), +- tls: None, ++ tls: None, + } + ); + } +@@ -371,7 +371,7 @@ url = "https://config.example.com" + .unwrap(), + ServerTarget::HttpUrl { + api_url: "https://cli.example.com".to_string(), +- tls: None, ++ tls: None, + } + ); + } +@@ -380,8 +380,8 @@ url = "https://config.example.com" + fn remote_target_uses_tls_from_config() { + let expected_tls = ClientTlsSettings { + cert: PathBuf::from("cert.pem"), +- key: PathBuf::from("key.pem"), +- ca: PathBuf::from("ca.pem"), ++ key: PathBuf::from("key.pem"), ++ ca: PathBuf::from("ca.pem"), + }; + let settings = parse_v2( + r#" +@@ -405,7 +405,7 @@ ca = "ca.pem" + .unwrap(), + Some(ServerTarget::HttpUrl { + api_url: "https://cli.example.com".to_string(), +- tls: Some(expected_tls), ++ tls: Some(expected_tls), + }) + ); + } +diff --git a/lib/crates/fabro-cli/tests/it/cmd/config.rs b/lib/crates/fabro-cli/tests/it/cmd/config.rs +index 50dcba53..33b2440c 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/config.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/config.rs +@@ -516,9 +516,10 @@ fn settings_local_explicit_workflow_path_uses_workflow_project_layers() { + assert!(auto_approve_enabled(&cfg)); + // v2 R30: run.prepare.steps replaces the whole ordered list across layers. + // The highest-precedence layer (workflow) wins. +- assert_eq!(run_prepare_commands(&cfg), vec![ +- "workflow-setup".to_string() +- ]); ++ assert_eq!( ++ run_prepare_commands(&cfg), ++ vec!["workflow-setup".to_string()] ++ ); + assert_eq!(run_sandbox(&cfg)["preserve"].as_bool(), Some(true)); + } + +diff --git a/lib/crates/fabro-cli/tests/it/cmd/fork.rs b/lib/crates/fabro-cli/tests/it/cmd/fork.rs +index 162bdf6a..cfea00b8 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/fork.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/fork.rs +@@ -84,10 +84,10 @@ fn fork_latest_prints_new_run_and_resume_hint() { + ); + let new_run_id = &new_run_ids[0]; + +- let new_head = git_stdout(&setup.repo_dir, &[ +- "rev-parse", +- &format!("fabro/run/{new_run_id}"), +- ]); ++ let new_head = git_stdout( ++ &setup.repo_dir, ++ &["rev-parse", &format!("fabro/run/{new_run_id}")], ++ ); + let expected_head = run_branch_commits(&setup.repo_dir, &setup.run.run_id) + .into_iter() + .last() +@@ -128,10 +128,10 @@ fn fork_from_earlier_checkpoint_uses_expected_sha() { + ); + let new_run_id = &new_run_ids[0]; + +- let new_head = git_stdout(&setup.repo_dir, &[ +- "rev-parse", +- &format!("fabro/run/{new_run_id}"), +- ]); ++ let new_head = git_stdout( ++ &setup.repo_dir, ++ &["rev-parse", &format!("fabro/run/{new_run_id}")], ++ ); + assert_eq!(new_head.trim(), expected_head); + + let checkpoint = git_show_json( +diff --git a/lib/crates/fabro-cli/tests/it/cmd/logs.rs b/lib/crates/fabro-cli/tests/it/cmd/logs.rs +index 0ec7bfe4..08749487 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/logs.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/logs.rs +@@ -91,14 +91,17 @@ fn logs_completed_run_outputs_raw_ndjson() { + + let events = parse_ndjson(&output.stdout); + assert_events_belong_to_run(&events, &run.run_id); +- assert_event_sequence_contains(&events, &[ +- "run.created", +- "run.running", +- "stage.started", +- "stage.completed", +- "run.completed", +- "sandbox.cleanup.completed", +- ]); ++ assert_event_sequence_contains( ++ &events, ++ &[ ++ "run.created", ++ "run.running", ++ "stage.started", ++ "stage.completed", ++ "run.completed", ++ "sandbox.cleanup.completed", ++ ], ++ ); + } + + #[test] +@@ -224,12 +227,15 @@ fn logs_follow_detached_run_streams_until_completion() { + + let events = parse_ndjson(&output.stdout); + assert_events_belong_to_run(&events, &run.run_id); +- assert_event_sequence_contains(&events, &[ +- "run.created", +- "run.running", +- "stage.started", +- "stage.completed", +- "run.completed", +- "sandbox.cleanup.completed", +- ]); ++ assert_event_sequence_contains( ++ &events, ++ &[ ++ "run.created", ++ "run.running", ++ "stage.started", ++ "stage.completed", ++ "run.completed", ++ "sandbox.cleanup.completed", ++ ], ++ ); + } +diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs b/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs +index 1f133881..ea80a7e8 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs +@@ -74,14 +74,14 @@ fn pr_view_reads_pull_request_from_store_without_pull_request_json() { + tool_call_id: None, + actor: None, + body: EventBody::PullRequestCreated(PullRequestCreatedProps { +- pr_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(), +- pr_number: 123, +- owner: "fabro-sh".to_string(), +- repo: "fabro".to_string(), ++ pr_url: "https://github.com/fabro-sh/fabro/pull/123".to_string(), ++ pr_number: 123, ++ owner: "fabro-sh".to_string(), ++ repo: "fabro".to_string(), + base_branch: "main".to_string(), + head_branch: "fabro/run/demo".to_string(), +- title: "Map the constellations".to_string(), +- draft: false, ++ title: "Map the constellations".to_string(), ++ draft: false, + }), + }; + client +diff --git a/lib/crates/fabro-cli/tests/it/cmd/resume.rs b/lib/crates/fabro-cli/tests/it/cmd/resume.rs +index 5a1a940d..aad477a1 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/resume.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/resume.rs +@@ -69,10 +69,10 @@ fn resume_rewound_run_succeeds() { + String::from_utf8_lossy(&rewind.stdout), + output_stderr(&rewind) + ); +- let rewound_head = git_stdout(&setup.repo_dir, &[ +- "rev-parse", +- &format!("fabro/run/{}", setup.run.run_id), +- ]); ++ let rewound_head = git_stdout( ++ &setup.repo_dir, ++ &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], ++ ); + + let mut resume_cmd = context.command(); + resume_cmd.current_dir(&setup.repo_dir); +@@ -109,10 +109,10 @@ fn resume_rewound_run_succeeds() { + std::fs::read_to_string(setup.repo_dir.join("story.txt")).unwrap(), + "line 1\n" + ); +- let resumed_head = git_stdout(&setup.repo_dir, &[ +- "rev-parse", +- &format!("fabro/run/{}", setup.run.run_id), +- ]); ++ let resumed_head = git_stdout( ++ &setup.repo_dir, ++ &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], ++ ); + assert_ne!(resumed_head.trim(), rewound_head.trim()); + } + +diff --git a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs b/lib/crates/fabro-cli/tests/it/cmd/rewind.rs +index 3122c81e..b5052f16 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/rewind.rs +@@ -99,10 +99,10 @@ fn rewind_target_updates_metadata_and_resume_hint() { + "); + assert!(output.status.success(), "rewind should succeed"); + +- let run_head = git_stdout(&setup.repo_dir, &[ +- "rev-parse", +- &format!("fabro/run/{}", setup.run.run_id), +- ]); ++ let run_head = git_stdout( ++ &setup.repo_dir, ++ &["rev-parse", &format!("fabro/run/{}", setup.run.run_id)], ++ ); + assert_eq!(run_head.trim(), expected_run_head); + + let mut list_cmd = context.command(); +diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs +index 01b2220b..f7560166 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/run.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs +@@ -97,7 +97,7 @@ fn seed_anthropic_vault(storage_dir: &std::path::Path, base_url: &str) { + "anthropic", + &serde_json::to_string(&AuthCredential { + provider: Provider::Anthropic, +- details: AuthDetails::ApiKey { ++ details: AuthDetails::ApiKey { + key: "vault-anthropic-key".to_string(), + }, + }) +diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs +index b8b814fb..cf980be8 100644 +--- a/lib/crates/fabro-cli/tests/it/cmd/support.rs ++++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs +@@ -35,23 +35,23 @@ struct RunSummaryRecord { + } + + pub(crate) struct RunSetup { +- pub(crate) run_id: String, ++ pub(crate) run_id: String, + pub(crate) run_dir: PathBuf, + } + + pub(crate) struct GitRunSetup { +- pub(crate) run: RunSetup, ++ pub(crate) run: RunSetup, + pub(crate) repo_dir: PathBuf, + pub(crate) base_sha: String, + } + + pub(crate) struct ProjectFixture { + pub(crate) project_dir: PathBuf, +- pub(crate) fabro_root: PathBuf, ++ pub(crate) fabro_root: PathBuf, + } + + pub(crate) struct WorkspaceRunSetup { +- pub(crate) run: RunSetup, ++ pub(crate) run: RunSetup, + pub(crate) workspace_dir: PathBuf, + } + +@@ -157,10 +157,10 @@ fn run_completed_dry_run(context: &TestContext, workflow: &Path) -> RunSetup { + run_dir: context.find_run_dir(&run_id), + run_id, + }; +- wait_for_event_names(&run_setup.run_dir, &[ +- "run.completed", +- "sandbox.cleanup.completed", +- ]); ++ wait_for_event_names( ++ &run_setup.run_dir, ++ &["run.completed", "sandbox.cleanup.completed"], ++ ); + run_setup + } + +@@ -659,7 +659,7 @@ fn block_on(future: impl std::future::Future) -> T { + + #[derive(Debug, serde::Deserialize)] + struct TestServerRecord { +- bind: Bind, ++ bind: Bind, + #[serde(default)] + dev_token_path: Option, + } +@@ -829,11 +829,10 @@ pub(crate) fn metadata_run_ids(repo_dir: &Path) -> BTreeSet { + } + + pub(crate) fn run_branch_commits(repo_dir: &Path, run_id: &str) -> Vec { +- git_stdout(repo_dir, &[ +- "rev-list", +- "--reverse", +- &format!("fabro/run/{run_id}"), +- ]) ++ git_stdout( ++ repo_dir, ++ &["rev-list", "--reverse", &format!("fabro/run/{run_id}")], ++ ) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) +@@ -846,11 +845,14 @@ pub(crate) fn run_branch_commits_since_base( + run_id: &str, + base_sha: &str, + ) -> Vec { +- git_stdout(repo_dir, &[ +- "rev-list", +- "--reverse", +- &format!("{base_sha}..fabro/run/{run_id}"), +- ]) ++ git_stdout( ++ repo_dir, ++ &[ ++ "rev-list", ++ "--reverse", ++ &format!("{base_sha}..fabro/run/{run_id}"), ++ ], ++ ) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) +@@ -1038,9 +1040,11 @@ fn setup_git_backed_run(context: &TestContext, workflow: GitWorkflowKind) -> Git + git_success(&repo_dir, &["config", "user.email", "test@example.com"]); + + write_text_file(&repo_dir.join("story.txt"), "line 1\n"); +- write_text_file(&repo_dir.join("flow.fabro"), match workflow { +- GitWorkflowKind::Changed => { +- r#"digraph Flow { ++ write_text_file( ++ &repo_dir.join("flow.fabro"), ++ match workflow { ++ GitWorkflowKind::Changed => { ++ r#"digraph Flow { + graph [goal="Edit a tracked file"]; + start [shape=Mdiamond]; + exit [shape=Msquare]; +@@ -1049,9 +1053,9 @@ fn setup_git_backed_run(context: &TestContext, workflow: GitWorkflowKind) -> Git + start -> step_one -> step_two -> exit; + } + "# +- } +- GitWorkflowKind::Noop => { +- r#"digraph Flow { ++ } ++ GitWorkflowKind::Noop => { ++ r#"digraph Flow { + graph [goal="Leave tracked files unchanged"]; + start [shape=Mdiamond]; + exit [shape=Msquare]; +@@ -1059,8 +1063,9 @@ fn setup_git_backed_run(context: &TestContext, workflow: GitWorkflowKind) -> Git + start -> check -> exit; + } + "# +- } +- }); ++ } ++ }, ++ ); + + git_success(&repo_dir, &["add", "story.txt", "flow.fabro"]); + git_success(&repo_dir, &["commit", "-qm", "init"]); +diff --git a/lib/crates/fabro-config/src/effective_settings.rs b/lib/crates/fabro-config/src/effective_settings.rs +index b16f2065..425e83cf 100644 +--- a/lib/crates/fabro-config/src/effective_settings.rs ++++ b/lib/crates/fabro-config/src/effective_settings.rs +@@ -25,10 +25,10 @@ pub enum EffectiveSettingsMode { + + #[derive(Clone, Debug, Default)] + pub struct EffectiveSettingsLayers { +- pub args: SettingsLayer, ++ pub args: SettingsLayer, + pub workflow: SettingsLayer, +- pub project: SettingsLayer, +- pub user: SettingsLayer, ++ pub project: SettingsLayer, ++ pub user: SettingsLayer, + } + + impl EffectiveSettingsLayers { +diff --git a/lib/crates/fabro-config/src/envfile.rs b/lib/crates/fabro-config/src/envfile.rs +index 67a02bdf..49a1b0f8 100644 +--- a/lib/crates/fabro-config/src/envfile.rs ++++ b/lib/crates/fabro-config/src/envfile.rs +@@ -137,10 +137,13 @@ mod tests { + let path = dir.path().join("server.env"); + std::fs::write(&path, "EXISTING=value\n").unwrap(); + +- let entries = merge_env_file(&path, [ +- ("SESSION_SECRET", "secret"), +- ("FABRO_JWT_PUBLIC_KEY", "jwt"), +- ]) ++ let entries = merge_env_file( ++ &path, ++ [ ++ ("SESSION_SECRET", "secret"), ++ ("FABRO_JWT_PUBLIC_KEY", "jwt"), ++ ], ++ ) + .unwrap(); + + assert_eq!(entries.get("EXISTING").map(String::as_str), Some("value")); +diff --git a/lib/crates/fabro-config/src/error.rs b/lib/crates/fabro-config/src/error.rs +index ead3042a..6061c8d6 100644 +--- a/lib/crates/fabro-config/src/error.rs ++++ b/lib/crates/fabro-config/src/error.rs +@@ -24,7 +24,7 @@ fn format_path_suffix(path: Option<&PathBuf>) -> String { + pub enum Error { + #[error("reading config file {path}: {source}")] + ReadFile { +- path: PathBuf, ++ path: PathBuf, + #[source] + source: std::io::Error, + }, +@@ -32,14 +32,14 @@ pub enum Error { + #[error("{context}{}: {source}", format_path_suffix(.path.as_ref()))] + ParseSettings { + context: &'static str, +- path: Option, ++ path: Option, + #[source] +- source: ParseError, ++ source: ParseError, + }, + + #[error("parsing TOML config at {path}: {source}")] + TomlParse { +- path: PathBuf, ++ path: PathBuf, + #[source] + source: TomlError, + }, +@@ -47,13 +47,13 @@ pub enum Error { + #[error("{context}:\n{}", format_resolve_errors(.errors))] + Resolve { + context: &'static str, +- errors: Vec, ++ errors: Vec, + }, + + #[error("missing required environment variable {var} for {field}")] + MissingEnvVar { +- field: String, +- var: String, ++ field: String, ++ var: String, + #[source] + source: std::env::VarError, + }, +diff --git a/lib/crates/fabro-config/src/merge.rs b/lib/crates/fabro-config/src/merge.rs +index a72aa2f6..a988de74 100644 +--- a/lib/crates/fabro-config/src/merge.rs ++++ b/lib/crates/fabro-config/src/merge.rs +@@ -32,12 +32,12 @@ use fabro_types::settings::workflow::WorkflowLayer; + #[must_use] + pub fn combine_files(lower: SettingsLayer, higher: SettingsLayer) -> SettingsLayer { + SettingsLayer { +- version: higher.version.or(lower.version), +- project: merge_option(lower.project, higher.project, combine_project), ++ version: higher.version.or(lower.version), ++ project: merge_option(lower.project, higher.project, combine_project), + workflow: merge_option(lower.workflow, higher.workflow, combine_workflow), +- run: merge_option(lower.run, higher.run, combine_run), +- cli: merge_option(lower.cli, higher.cli, combine_cli), +- server: merge_option(lower.server, higher.server, combine_server), ++ run: merge_option(lower.run, higher.run, combine_run), ++ cli: merge_option(lower.cli, higher.cli, combine_cli), ++ server: merge_option(lower.server, higher.server, combine_server), + features: replace_if_some(lower.features, higher.features), + } + } +@@ -76,10 +76,10 @@ fn merge_string_map_sticky( + + fn combine_project(lower: ProjectLayer, higher: ProjectLayer) -> ProjectLayer { + ProjectLayer { +- name: higher.name.or(lower.name), ++ name: higher.name.or(lower.name), + description: higher.description.or(lower.description), +- directory: higher.directory.or(lower.directory), +- metadata: merge_string_map_replace(lower.metadata, higher.metadata), ++ directory: higher.directory.or(lower.directory), ++ metadata: merge_string_map_replace(lower.metadata, higher.metadata), + } + } + +@@ -87,10 +87,10 @@ fn combine_project(lower: ProjectLayer, higher: ProjectLayer) -> ProjectLayer { + + fn combine_workflow(lower: WorkflowLayer, higher: WorkflowLayer) -> WorkflowLayer { + WorkflowLayer { +- name: higher.name.or(lower.name), ++ name: higher.name.or(lower.name), + description: higher.description.or(lower.description), +- graph: higher.graph.or(lower.graph), +- metadata: merge_string_map_replace(lower.metadata, higher.metadata), ++ graph: higher.graph.or(lower.graph), ++ metadata: merge_string_map_replace(lower.metadata, higher.metadata), + } + } + +@@ -98,30 +98,30 @@ fn combine_workflow(lower: WorkflowLayer, higher: WorkflowLayer) -> WorkflowLaye + + fn combine_run(lower: RunLayer, higher: RunLayer) -> RunLayer { + RunLayer { +- goal: higher.goal.or(lower.goal), +- working_dir: higher.working_dir.or(lower.working_dir), +- metadata: merge_string_map_replace(lower.metadata, higher.metadata), +- inputs: higher.inputs.or(lower.inputs), +- model: merge_option(lower.model, higher.model, combine_run_model), +- git: merge_option(lower.git, higher.git, combine_run_git), +- prepare: merge_option(lower.prepare, higher.prepare, combine_run_prepare), +- execution: merge_option(lower.execution, higher.execution, combine_run_execution), +- checkpoint: merge_option(lower.checkpoint, higher.checkpoint, combine_run_checkpoint), +- sandbox: merge_option(lower.sandbox, higher.sandbox, combine_run_sandbox), ++ goal: higher.goal.or(lower.goal), ++ working_dir: higher.working_dir.or(lower.working_dir), ++ metadata: merge_string_map_replace(lower.metadata, higher.metadata), ++ inputs: higher.inputs.or(lower.inputs), ++ model: merge_option(lower.model, higher.model, combine_run_model), ++ git: merge_option(lower.git, higher.git, combine_run_git), ++ prepare: merge_option(lower.prepare, higher.prepare, combine_run_prepare), ++ execution: merge_option(lower.execution, higher.execution, combine_run_execution), ++ checkpoint: merge_option(lower.checkpoint, higher.checkpoint, combine_run_checkpoint), ++ sandbox: merge_option(lower.sandbox, higher.sandbox, combine_run_sandbox), + notifications: combine_notifications(lower.notifications, higher.notifications), +- interviews: merge_option(lower.interviews, higher.interviews, combine_interviews), +- agent: merge_option(lower.agent, higher.agent, combine_run_agent), +- hooks: combine_hooks(lower.hooks, higher.hooks), +- scm: merge_option(lower.scm, higher.scm, combine_run_scm), +- pull_request: merge_option(lower.pull_request, higher.pull_request, combine_run_pr), +- artifacts: replace_if_some(lower.artifacts, higher.artifacts), ++ interviews: merge_option(lower.interviews, higher.interviews, combine_interviews), ++ agent: merge_option(lower.agent, higher.agent, combine_run_agent), ++ hooks: combine_hooks(lower.hooks, higher.hooks), ++ scm: merge_option(lower.scm, higher.scm, combine_run_scm), ++ pull_request: merge_option(lower.pull_request, higher.pull_request, combine_run_pr), ++ artifacts: replace_if_some(lower.artifacts, higher.artifacts), + } + } + + fn combine_run_model(lower: RunModelLayer, higher: RunModelLayer) -> RunModelLayer { + RunModelLayer { +- provider: higher.provider.or(lower.provider), +- name: higher.name.or(lower.name), ++ provider: higher.provider.or(lower.provider), ++ name: higher.name.or(lower.name), + fallbacks: splice_model_fallbacks(lower.fallbacks, higher.fallbacks), + } + } +@@ -163,7 +163,7 @@ fn combine_run_git(lower: RunGitLayer, higher: RunGitLayer) -> RunGitLayer { + + fn combine_git_author(lower: GitAuthorLayer, higher: GitAuthorLayer) -> GitAuthorLayer { + GitAuthorLayer { +- name: higher.name.or(lower.name), ++ name: higher.name.or(lower.name), + email: higher.email.or(lower.email), + } + } +@@ -175,9 +175,9 @@ fn combine_run_prepare(_lower: RunPrepareLayer, higher: RunPrepareLayer) -> RunP + + fn combine_run_execution(lower: RunExecutionLayer, higher: RunExecutionLayer) -> RunExecutionLayer { + RunExecutionLayer { +- mode: higher.mode.or(lower.mode), ++ mode: higher.mode.or(lower.mode), + approval: higher.approval.or(lower.approval), +- retros: higher.retros.or(lower.retros), ++ retros: higher.retros.or(lower.retros), + } + } + +@@ -195,13 +195,13 @@ fn combine_run_checkpoint( + + fn combine_run_sandbox(lower: RunSandboxLayer, higher: RunSandboxLayer) -> RunSandboxLayer { + RunSandboxLayer { +- provider: higher.provider.or(lower.provider), +- preserve: higher.preserve.or(lower.preserve), ++ provider: higher.provider.or(lower.provider), ++ preserve: higher.preserve.or(lower.preserve), + devcontainer: higher.devcontainer.or(lower.devcontainer), + // Sticky merge-by-key for run.sandbox.env per R71. +- env: merge_string_map_sticky(lower.env, higher.env), +- local: higher.local.or(lower.local), +- daytona: merge_option(lower.daytona, higher.daytona, combine_daytona), ++ env: merge_string_map_sticky(lower.env, higher.env), ++ local: higher.local.or(lower.local), ++ daytona: merge_option(lower.daytona, higher.daytona, combine_daytona), + } + } + +@@ -209,10 +209,10 @@ fn combine_daytona(lower: DaytonaSandboxLayer, higher: DaytonaSandboxLayer) -> D + DaytonaSandboxLayer { + auto_stop_interval: higher.auto_stop_interval.or(lower.auto_stop_interval), + // Sticky merge-by-key for provider-native labels per R71. +- labels: merge_string_map_sticky(lower.labels, higher.labels), +- snapshot: higher.snapshot.or(lower.snapshot), +- network: higher.network.or(lower.network), +- skip_clone: higher.skip_clone.or(lower.skip_clone), ++ labels: merge_string_map_sticky(lower.labels, higher.labels), ++ snapshot: higher.snapshot.or(lower.snapshot), ++ network: higher.network.or(lower.network), ++ skip_clone: higher.skip_clone.or(lower.skip_clone), + } + } + +@@ -238,12 +238,12 @@ fn combine_notification_route( + higher: NotificationRouteLayer, + ) -> NotificationRouteLayer { + NotificationRouteLayer { +- enabled: higher.enabled.or(lower.enabled), ++ enabled: higher.enabled.or(lower.enabled), + provider: higher.provider.or(lower.provider), +- events: splice_events(lower.events, higher.events), +- slack: higher.slack.or(lower.slack), +- discord: higher.discord.or(lower.discord), +- teams: higher.teams.or(lower.teams), ++ events: splice_events(lower.events, higher.events), ++ slack: higher.slack.or(lower.slack), ++ discord: higher.discord.or(lower.discord), ++ teams: higher.teams.or(lower.teams), + } + } + +@@ -276,9 +276,9 @@ fn splice_events(lower: Vec, higher: Vec) -> Vec + fn combine_interviews(lower: InterviewsLayer, higher: InterviewsLayer) -> InterviewsLayer { + InterviewsLayer { + provider: higher.provider.or(lower.provider), +- slack: higher.slack.or(lower.slack), +- discord: higher.discord.or(lower.discord), +- teams: higher.teams.or(lower.teams), ++ slack: higher.slack.or(lower.slack), ++ discord: higher.discord.or(lower.discord), ++ teams: higher.teams.or(lower.teams), + } + } + +@@ -286,7 +286,7 @@ fn combine_run_agent(lower: RunAgentLayer, higher: RunAgentLayer) -> RunAgentLay + RunAgentLayer { + permissions: higher.permissions.or(lower.permissions), + // MCP entries: field-merge per key. Higher replaces lower for same keys. +- mcps: merge_string_map_sticky(lower.mcps, higher.mcps), ++ mcps: merge_string_map_sticky(lower.mcps, higher.mcps), + } + } + +@@ -321,18 +321,18 @@ fn combine_hooks(lower: Vec, higher: Vec) -> Vec RunScmLayer { + RunScmLayer { +- provider: higher.provider.or(lower.provider), +- owner: higher.owner.or(lower.owner), ++ provider: higher.provider.or(lower.provider), ++ owner: higher.owner.or(lower.owner), + repository: higher.repository.or(lower.repository), +- github: higher.github.or(lower.github), ++ github: higher.github.or(lower.github), + } + } + + fn combine_run_pr(lower: RunPullRequestLayer, higher: RunPullRequestLayer) -> RunPullRequestLayer { + RunPullRequestLayer { +- enabled: higher.enabled.or(lower.enabled), +- draft: higher.draft.or(lower.draft), +- auto_merge: higher.auto_merge.or(lower.auto_merge), ++ enabled: higher.enabled.or(lower.enabled), ++ draft: higher.draft.or(lower.draft), ++ auto_merge: higher.auto_merge.or(lower.auto_merge), + merge_strategy: higher.merge_strategy.or(lower.merge_strategy), + } + } +@@ -341,10 +341,10 @@ fn combine_run_pr(lower: RunPullRequestLayer, higher: RunPullRequestLayer) -> Ru + + fn combine_cli(lower: CliLayer, higher: CliLayer) -> CliLayer { + CliLayer { +- target: merge_option(lower.target, higher.target, combine_cli_target), +- auth: higher.auth.or(lower.auth), +- exec: merge_option(lower.exec, higher.exec, combine_cli_exec), +- output: merge_option(lower.output, higher.output, combine_cli_output), ++ target: merge_option(lower.target, higher.target, combine_cli_target), ++ auth: higher.auth.or(lower.auth), ++ exec: merge_option(lower.exec, higher.exec, combine_cli_exec), ++ output: merge_option(lower.output, higher.output, combine_cli_output), + updates: merge_option(lower.updates, higher.updates, combine_cli_updates), + logging: higher.logging.or(lower.logging), + } +@@ -358,8 +358,8 @@ fn combine_cli_target(_lower: CliTargetLayer, higher: CliTargetLayer) -> CliTarg + fn combine_cli_exec(lower: CliExecLayer, higher: CliExecLayer) -> CliExecLayer { + CliExecLayer { + prevent_idle_sleep: higher.prevent_idle_sleep.or(lower.prevent_idle_sleep), +- model: merge_option(lower.model, higher.model, combine_cli_exec_model), +- agent: merge_option(lower.agent, higher.agent, combine_cli_exec_agent), ++ model: merge_option(lower.model, higher.model, combine_cli_exec_model), ++ agent: merge_option(lower.agent, higher.agent, combine_cli_exec_agent), + } + } + +@@ -369,7 +369,7 @@ fn combine_cli_exec_model( + ) -> CliExecModelLayer { + CliExecModelLayer { + provider: higher.provider.or(lower.provider), +- name: higher.name.or(lower.name), ++ name: higher.name.or(lower.name), + } + } + +@@ -379,13 +379,13 @@ fn combine_cli_exec_agent( + ) -> CliExecAgentLayer { + CliExecAgentLayer { + permissions: higher.permissions.or(lower.permissions), +- mcps: merge_string_map_sticky(lower.mcps, higher.mcps), ++ mcps: merge_string_map_sticky(lower.mcps, higher.mcps), + } + } + + fn combine_cli_output(lower: CliOutputLayer, higher: CliOutputLayer) -> CliOutputLayer { + CliOutputLayer { +- format: higher.format.or(lower.format), ++ format: higher.format.or(lower.format), + verbosity: higher.verbosity.or(lower.verbosity), + } + } +@@ -400,15 +400,15 @@ fn combine_cli_updates(lower: CliUpdatesLayer, higher: CliUpdatesLayer) -> CliUp + + fn combine_server(lower: ServerLayer, higher: ServerLayer) -> ServerLayer { + ServerLayer { +- listen: merge_option(lower.listen, higher.listen, combine_listen), +- api: higher.api.or(lower.api), +- web: merge_option(lower.web, higher.web, combine_server_web), +- auth: merge_option(lower.auth, higher.auth, combine_server_auth), +- storage: merge_option(lower.storage, higher.storage, combine_server_storage), +- artifacts: merge_option(lower.artifacts, higher.artifacts, combine_server_artifacts), +- slatedb: merge_option(lower.slatedb, higher.slatedb, combine_server_slatedb), +- scheduler: merge_option(lower.scheduler, higher.scheduler, combine_server_scheduler), +- logging: higher.logging.or(lower.logging), ++ listen: merge_option(lower.listen, higher.listen, combine_listen), ++ api: higher.api.or(lower.api), ++ web: merge_option(lower.web, higher.web, combine_server_web), ++ auth: merge_option(lower.auth, higher.auth, combine_server_auth), ++ storage: merge_option(lower.storage, higher.storage, combine_server_storage), ++ artifacts: merge_option(lower.artifacts, higher.artifacts, combine_server_artifacts), ++ slatedb: merge_option(lower.slatedb, higher.slatedb, combine_server_slatedb), ++ scheduler: merge_option(lower.scheduler, higher.scheduler, combine_server_scheduler), ++ logging: higher.logging.or(lower.logging), + integrations: merge_option( + lower.integrations, + higher.integrations, +@@ -425,14 +425,14 @@ fn combine_listen(_lower: ServerListenLayer, higher: ServerListenLayer) -> Serve + fn combine_server_web(lower: ServerWebLayer, higher: ServerWebLayer) -> ServerWebLayer { + ServerWebLayer { + enabled: higher.enabled.or(lower.enabled), +- url: higher.url.or(lower.url), ++ url: higher.url.or(lower.url), + } + } + + fn combine_server_auth(lower: ServerAuthLayer, higher: ServerAuthLayer) -> ServerAuthLayer { + ServerAuthLayer { + methods: higher.methods.or(lower.methods), +- github: higher.github.or(lower.github), ++ github: higher.github.or(lower.github), + } + } + +@@ -451,9 +451,9 @@ fn combine_server_artifacts( + ) -> ServerArtifactsLayer { + ServerArtifactsLayer { + provider: higher.provider.or(lower.provider), +- prefix: higher.prefix.or(lower.prefix), +- local: higher.local.or(lower.local), +- s3: higher.s3.or(lower.s3), ++ prefix: higher.prefix.or(lower.prefix), ++ local: higher.local.or(lower.local), ++ s3: higher.s3.or(lower.s3), + } + } + +@@ -462,12 +462,12 @@ fn combine_server_slatedb( + higher: ServerSlateDbLayer, + ) -> ServerSlateDbLayer { + ServerSlateDbLayer { +- provider: higher.provider.or(lower.provider), +- prefix: higher.prefix.or(lower.prefix), ++ provider: higher.provider.or(lower.provider), ++ prefix: higher.prefix.or(lower.prefix), + flush_interval: higher.flush_interval.or(lower.flush_interval), +- local: higher.local.or(lower.local), +- s3: higher.s3.or(lower.s3), +- disk_cache: higher.disk_cache.or(lower.disk_cache), ++ local: higher.local.or(lower.local), ++ s3: higher.s3.or(lower.s3), ++ disk_cache: higher.disk_cache.or(lower.disk_cache), + } + } + +@@ -485,10 +485,10 @@ fn combine_server_integrations( + higher: ServerIntegrationsLayer, + ) -> ServerIntegrationsLayer { + ServerIntegrationsLayer { +- github: higher.github.or(lower.github), +- slack: higher.slack.or(lower.slack), ++ github: higher.github.or(lower.github), ++ slack: higher.slack.or(lower.slack), + discord: higher.discord.or(lower.discord), +- teams: higher.teams.or(lower.teams), ++ teams: higher.teams.or(lower.teams), + } + } + +diff --git a/lib/crates/fabro-config/src/parse.rs b/lib/crates/fabro-config/src/parse.rs +index 219c0493..ea975708 100644 +--- a/lib/crates/fabro-config/src/parse.rs ++++ b/lib/crates/fabro-config/src/parse.rs +@@ -66,7 +66,7 @@ pub fn parse_settings_layer(input: &str) -> Result { + for key in table.keys() { + if !ALLOWED_TOP_LEVEL_KEYS.contains(&key.as_str()) { + return Err(ParseError::UnknownTopLevelKey { +- key: key.clone(), ++ key: key.clone(), + hint: rename_hint(key), + }); + } +diff --git a/lib/crates/fabro-config/src/project.rs b/lib/crates/fabro-config/src/project.rs +index 087fd619..862d8ad3 100644 +--- a/lib/crates/fabro-config/src/project.rs ++++ b/lib/crates/fabro-config/src/project.rs +@@ -21,10 +21,10 @@ const CONFIG_FILENAME: &str = ".fabro/project.toml"; + #[derive(Clone, Debug)] + pub struct WorkflowPathResolution { + pub resolved_workflow_path: PathBuf, +- pub dot_path: PathBuf, +- pub workflow_config: Option, +- pub workflow_toml_path: Option, +- pub workflow_slug: Option, ++ pub dot_path: PathBuf, ++ pub workflow_config: Option, ++ pub workflow_toml_path: Option, ++ pub workflow_slug: Option, + } + + /// Parse a project config from a TOML string. +@@ -222,8 +222,8 @@ fn user_workflows_dir() -> PathBuf { + /// Metadata about a discovered workflow. + #[derive(Clone, Debug, Serialize)] + pub struct WorkflowInfo { +- pub name: String, +- pub goal: Option, ++ pub name: String, ++ pub goal: Option, + pub source: WorkflowSource, + } + +diff --git a/lib/crates/fabro-config/src/resolve/cli.rs b/lib/crates/fabro-config/src/resolve/cli.rs +index de7c8303..fc838620 100644 +--- a/lib/crates/fabro-config/src/resolve/cli.rs ++++ b/lib/crates/fabro-config/src/resolve/cli.rs +@@ -8,13 +8,13 @@ use super::{ResolveError, require_interp}; + + pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec) -> CliSettings { + CliSettings { +- target: resolve_target(layer.target.as_ref(), errors), +- auth: CliAuthSettings { ++ target: resolve_target(layer.target.as_ref(), errors), ++ auth: CliAuthSettings { + strategy: layer.auth.as_ref().and_then(|auth| auth.strategy), + }, +- exec: resolve_exec(layer.exec.as_ref()), +- output: CliOutputSettings { +- format: layer ++ exec: resolve_exec(layer.exec.as_ref()), ++ output: CliOutputSettings { ++ format: layer + .output + .as_ref() + .and_then(|output| output.format) +@@ -50,8 +50,8 @@ fn resolve_target( + url: require_interp(url.as_ref(), "cli.target.url", errors), + tls: tls.as_ref().map(|tls| CliTargetTlsSettings { + cert: require_interp(tls.cert.as_ref(), "cli.target.tls.cert", errors), +- key: require_interp(tls.key.as_ref(), "cli.target.tls.key", errors), +- ca: require_interp(tls.ca.as_ref(), "cli.target.tls.ca", errors), ++ key: require_interp(tls.key.as_ref(), "cli.target.tls.key", errors), ++ ca: require_interp(tls.ca.as_ref(), "cli.target.tls.ca", errors), + }), + }), + Some(CliTargetLayer::Unix { path }) => Some(CliTargetSettings::Unix { +@@ -68,13 +68,13 @@ fn resolve_exec(exec: Option<&CliExecLayer>) -> CliExecSettings { + prevent_idle_sleep: exec + .prevent_idle_sleep + .expect("defaults.toml should provide cli.exec.prevent_idle_sleep"), +- model: CliExecModelSettings { ++ model: CliExecModelSettings { + provider: exec.model.as_ref().and_then(|model| model.provider.clone()), +- name: exec.model.as_ref().and_then(|model| model.name.clone()), ++ name: exec.model.as_ref().and_then(|model| model.name.clone()), + }, +- agent: CliExecAgentSettings { ++ agent: CliExecAgentSettings { + permissions: exec.agent.as_ref().and_then(|agent| agent.permissions), +- mcps: exec ++ mcps: exec + .agent + .as_ref() + .map(|agent| { +diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs +index b0ac5c82..0a8a908b 100644 +--- a/lib/crates/fabro-config/src/resolve/mod.rs ++++ b/lib/crates/fabro-config/src/resolve/mod.rs +@@ -31,11 +31,11 @@ pub fn resolve(file: &SettingsLayer) -> Result> { + let features_layer = file.features.clone().unwrap_or_default(); + + let settings = Settings { +- project: resolve_project(&project_layer, &mut errors), ++ project: resolve_project(&project_layer, &mut errors), + workflow: resolve_workflow(&workflow_layer, &mut errors), +- run: resolve_run(&run_layer, &mut errors), +- cli: resolve_cli(&cli_layer, &mut errors), +- server: resolve_server(&server_layer, &mut errors), ++ run: resolve_run(&run_layer, &mut errors), ++ cli: resolve_cli(&cli_layer, &mut errors), ++ server: resolve_server(&server_layer, &mut errors), + features: resolve_features(&features_layer, &mut errors), + }; + +@@ -99,7 +99,7 @@ pub(crate) fn parse_socket_addr( + Ok(address) => address, + Err(err) => { + errors.push(ResolveError::ParseFailure { +- path: path.to_string(), ++ path: path.to_string(), + reason: err.to_string(), + }); + std::net::SocketAddr::from(([127, 0, 0, 1], 0)) +@@ -166,7 +166,7 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" + mcps.get("stdio").map(|mcp| &mcp.transport), + Some(&McpTransport::Stdio { + command: vec!["fabro-mcp".to_string(), "--stdio".to_string()], +- env: HashMap::from([( ++ env: HashMap::from([( + "TOKEN".to_string(), + "Bearer {{ env.MCP_STDIO_TOKEN }}".to_string(), + )]), +@@ -175,7 +175,7 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" + assert_eq!( + mcps.get("http").map(|mcp| &mcp.transport), + Some(&McpTransport::Http { +- url: "https://mcp.example.com".to_string(), ++ url: "https://mcp.example.com".to_string(), + headers: HashMap::from([( + "Authorization".to_string(), + "Bearer {{ env.MCP_HTTP_TOKEN }}".to_string(), +@@ -186,8 +186,8 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" + mcps.get("sandbox").map(|mcp| &mcp.transport), + Some(&McpTransport::Sandbox { + command: vec!["fabro-mcp".to_string(), "--sandbox".to_string()], +- port: 3333, +- env: HashMap::from([( ++ port: 3333, ++ env: HashMap::from([( + "TOKEN".to_string(), + "{{ env.MCP_SANDBOX_TOKEN }}".to_string(), + )]), +@@ -203,13 +203,13 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}" + assert_eq!( + hook.resolved_hook_type().as_deref(), + Some(&HookType::Http { +- url: "https://hooks.example.com".to_string(), +- headers: Some(HashMap::from([( ++ url: "https://hooks.example.com".to_string(), ++ headers: Some(HashMap::from([( + "Authorization".to_string(), + "Bearer {{ env.HOOK_TOKEN }}".to_string(), + )])), + allowed_env_vars: Vec::new(), +- tls: TlsMode::Verify, ++ tls: TlsMode::Verify, + }) + ); + } +diff --git a/lib/crates/fabro-config/src/resolve/project.rs b/lib/crates/fabro-config/src/resolve/project.rs +index dbed5929..65b15a09 100644 +--- a/lib/crates/fabro-config/src/resolve/project.rs ++++ b/lib/crates/fabro-config/src/resolve/project.rs +@@ -4,12 +4,12 @@ use super::ResolveError; + + pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec) -> ProjectSettings { + ProjectSettings { +- name: layer.name.clone(), ++ name: layer.name.clone(), + description: layer.description.clone(), +- directory: layer ++ directory: layer + .directory + .clone() + .expect("defaults.toml should provide project.directory"), +- metadata: layer.metadata.clone(), ++ metadata: layer.metadata.clone(), + } + } +diff --git a/lib/crates/fabro-config/src/resolve/run.rs b/lib/crates/fabro-config/src/resolve/run.rs +index 35371564..a09e0a16 100644 +--- a/lib/crates/fabro-config/src/resolve/run.rs ++++ b/lib/crates/fabro-config/src/resolve/run.rs +@@ -17,32 +17,32 @@ use super::ResolveError; + + pub fn resolve_run(layer: &RunLayer, errors: &mut Vec) -> RunSettings { + RunSettings { +- goal: resolve_goal(layer.goal.as_ref()), +- working_dir: layer.working_dir.clone(), +- metadata: layer.metadata.clone(), +- inputs: layer.inputs.clone().unwrap_or_default(), +- model: resolve_model(layer.model.as_ref()), +- git: resolve_git(layer.git.as_ref()), +- prepare: resolve_prepare(layer.prepare.as_ref(), errors), +- execution: resolve_execution(layer.execution.as_ref()), +- checkpoint: resolve_checkpoint(layer.checkpoint.as_ref()), +- sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors), ++ goal: resolve_goal(layer.goal.as_ref()), ++ working_dir: layer.working_dir.clone(), ++ metadata: layer.metadata.clone(), ++ inputs: layer.inputs.clone().unwrap_or_default(), ++ model: resolve_model(layer.model.as_ref()), ++ git: resolve_git(layer.git.as_ref()), ++ prepare: resolve_prepare(layer.prepare.as_ref(), errors), ++ execution: resolve_execution(layer.execution.as_ref()), ++ checkpoint: resolve_checkpoint(layer.checkpoint.as_ref()), ++ sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors), + notifications: layer + .notifications + .iter() + .map(|(name, route)| (name.clone(), resolve_notification_route(route))) + .collect(), +- interviews: resolve_interviews(layer.interviews.as_ref()), +- agent: resolve_agent(layer.agent.as_ref()), +- hooks: layer ++ interviews: resolve_interviews(layer.interviews.as_ref()), ++ agent: resolve_agent(layer.agent.as_ref()), ++ hooks: layer + .hooks + .iter() + .enumerate() + .map(|(index, hook)| resolve_hook(hook, index, errors)) + .collect(), +- scm: resolve_scm(layer.scm.as_ref()), +- pull_request: resolve_pull_request(layer.pull_request.as_ref()), +- artifacts: resolve_artifacts(layer.artifacts.as_ref()), ++ scm: resolve_scm(layer.scm.as_ref()), ++ pull_request: resolve_pull_request(layer.pull_request.as_ref()), ++ artifacts: resolve_artifacts(layer.artifacts.as_ref()), + } + } + +@@ -59,8 +59,8 @@ fn resolve_model(model: Option<&RunModelLayer>) -> RunModelSettings { + }; + + RunModelSettings { +- provider: model.provider.clone(), +- name: model.name.clone(), ++ provider: model.provider.clone(), ++ name: model.name.clone(), + fallbacks: model + .fallbacks + .iter() +@@ -76,7 +76,7 @@ fn resolve_git(git: Option<&RunGitLayer>) -> RunGitSettings { + RunGitSettings { + author: git.and_then(|git| { + git.author.as_ref().map(|author| GitAuthorSettings { +- name: author.name.clone(), ++ name: author.name.clone(), + email: author.email.clone(), + }) + }), +@@ -100,7 +100,7 @@ fn resolve_prepare( + .join(" "), + ), + (Some(_), Some(_)) | (None, None) => errors.push(ResolveError::Invalid { +- path: format!("run.prepare.steps[{index}]"), ++ path: format!("run.prepare.steps[{index}]"), + reason: "exactly one of script or command must be set".to_string(), + }), + } +@@ -118,13 +118,13 @@ fn resolve_execution(execution: Option<&RunExecutionLayer>) -> RunExecutionSetti + let execution = execution.expect("defaults.toml should provide run.execution defaults"); + + RunExecutionSettings { +- mode: execution ++ mode: execution + .mode + .expect("defaults.toml should provide run.execution.mode"), + approval: execution + .approval + .expect("defaults.toml should provide run.execution.approval"), +- retros: execution ++ retros: execution + .retros + .expect("defaults.toml should provide run.execution.retros"), + } +@@ -151,7 +151,7 @@ fn resolve_sandbox( + match provider.as_str() { + "local" | "docker" | "daytona" => {} + other => errors.push(ResolveError::Invalid { +- path: "run.sandbox.provider".to_string(), ++ path: "run.sandbox.provider".to_string(), + reason: format!("unknown sandbox provider: {other}"), + }), + } +@@ -186,13 +186,13 @@ fn resolve_local_sandbox(sandbox: &RunSandboxLayer) -> LocalSandboxSettings { + fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings { + DaytonaSettings { + auto_stop_interval: daytona.auto_stop_interval, +- labels: daytona.labels.clone(), +- snapshot: daytona.snapshot.as_ref().and_then(|snapshot| { ++ labels: daytona.labels.clone(), ++ snapshot: daytona.snapshot.as_ref().and_then(|snapshot| { + snapshot.name.as_ref().map(|name| DaytonaSnapshotSettings { +- name: name.clone(), +- cpu: snapshot.cpu, +- memory_gb: snapshot.memory.map(|size| size_to_gb_i32(size.as_bytes())), +- disk_gb: snapshot.disk.map(|size| size_to_gb_i32(size.as_bytes())), ++ name: name.clone(), ++ cpu: snapshot.cpu, ++ memory_gb: snapshot.memory.map(|size| size_to_gb_i32(size.as_bytes())), ++ disk_gb: snapshot.disk.map(|size| size_to_gb_i32(size.as_bytes())), + dockerfile: snapshot + .dockerfile + .as_ref() +@@ -206,16 +206,16 @@ fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings { + }), + }) + }), +- network: daytona.network.clone(), +- skip_clone: daytona.skip_clone.unwrap_or(false), ++ network: daytona.network.clone(), ++ skip_clone: daytona.skip_clone.unwrap_or(false), + } + } + + fn resolve_notification_route(route: &NotificationRouteLayer) -> NotificationRouteSettings { + NotificationRouteSettings { +- enabled: route.enabled.unwrap_or(false), ++ enabled: route.enabled.unwrap_or(false), + provider: route.provider.clone(), +- events: route ++ events: route + .events + .iter() + .filter_map(|event| match event { +@@ -223,9 +223,9 @@ fn resolve_notification_route(route: &NotificationRouteLayer) -> NotificationRou + StringOrSplice::Splice => None, + }) + .collect(), +- slack: route.slack.as_ref().map(resolve_notification_provider), +- discord: route.discord.as_ref().map(resolve_notification_provider), +- teams: route.teams.as_ref().map(resolve_notification_provider), ++ slack: route.slack.as_ref().map(resolve_notification_provider), ++ discord: route.discord.as_ref().map(resolve_notification_provider), ++ teams: route.teams.as_ref().map(resolve_notification_provider), + } + } + +@@ -244,9 +244,9 @@ fn resolve_interviews(interviews: Option<&InterviewsLayer>) -> RunInterviewsSett + + RunInterviewsSettings { + provider: interviews.provider.clone(), +- slack: interviews.slack.as_ref().map(resolve_interview_provider), +- discord: interviews.discord.as_ref().map(resolve_interview_provider), +- teams: interviews.teams.as_ref().map(resolve_interview_provider), ++ slack: interviews.slack.as_ref().map(resolve_interview_provider), ++ discord: interviews.discord.as_ref().map(resolve_interview_provider), ++ teams: interviews.teams.as_ref().map(resolve_interview_provider), + } + } + +@@ -263,7 +263,7 @@ fn resolve_agent(agent: Option<&RunAgentLayer>) -> RunAgentSettings { + + RunAgentSettings { + permissions: agent.permissions, +- mcps: agent ++ mcps: agent + .mcps + .iter() + .map(|(name, entry)| (name.clone(), resolve_mcp_entry(name, entry))) +@@ -280,13 +280,13 @@ pub(crate) fn resolve_mcp_entry(name: &str, entry: &McpEntryLayer) -> McpServerS + .. + } => McpTransport::Stdio { + command: resolve_mcp_command(script.as_ref(), command.as_ref()), +- env: env ++ env: env + .iter() + .map(|(key, value)| (key.clone(), value.as_source())) + .collect(), + }, + McpEntryLayer::Http { url, headers, .. } => McpTransport::Http { +- url: url.as_source(), ++ url: url.as_source(), + headers: headers + .iter() + .map(|(key, value)| (key.clone(), value.as_source())) +@@ -300,8 +300,8 @@ pub(crate) fn resolve_mcp_entry(name: &str, entry: &McpEntryLayer) -> McpServerS + .. + } => McpTransport::Sandbox { + command: resolve_mcp_command(script.as_ref(), command.as_ref()), +- port: *port, +- env: env ++ port: *port, ++ env: env + .iter() + .map(|(key, value)| (key.clone(), value.as_source())) + .collect(), +@@ -362,7 +362,7 @@ fn resolve_hook(hook: &HookEntry, index: usize, errors: &mut Vec) + + if variants != 1 { + errors.push(ResolveError::Invalid { +- path: format!("run.hooks[{index}]"), ++ path: format!("run.hooks[{index}]"), + reason: "exactly one hook transport must be configured".to_string(), + }); + } +@@ -426,19 +426,19 @@ fn resolve_hook_type(hook: &HookEntry) -> Option { + + if hook.agent == Some(HookAgentMarker::Enabled) { + return Some(HookType::Agent { +- prompt: hook ++ prompt: hook + .prompt + .as_ref() + .map(InterpString::as_source) + .unwrap_or_default(), +- model: hook.model.as_ref().map(InterpString::as_source), ++ model: hook.model.as_ref().map(InterpString::as_source), + max_tool_rounds: hook.max_tool_rounds, + }); + } + + hook.prompt.as_ref().map(|prompt| HookType::Prompt { + prompt: prompt.as_source(), +- model: hook.model.as_ref().map(InterpString::as_source), ++ model: hook.model.as_ref().map(InterpString::as_source), + }) + } + +@@ -448,10 +448,10 @@ fn resolve_scm(scm: Option<&RunScmLayer>) -> RunScmSettings { + }; + + RunScmSettings { +- provider: scm.provider.clone(), +- owner: scm.owner.clone(), ++ provider: scm.provider.clone(), ++ owner: scm.owner.clone(), + repository: scm.repository.clone(), +- github: scm.github.as_ref().map(|_| ScmGitHubSettings), ++ github: scm.github.as_ref().map(|_| ScmGitHubSettings), + } + } + +@@ -462,9 +462,9 @@ fn resolve_pull_request(pull_request: Option<&RunPullRequestLayer>) -> Option, layer: Option<&ServerWebLayer>) -> + enabled: layer + .enabled + .expect("defaults.toml should provide server.web.enabled"), +- url: layer ++ url: layer + .url + .clone() + .expect("defaults.toml should provide server.web.url"), +@@ -124,7 +124,7 @@ fn resolve_auth( + .unwrap_or_else(|| vec![ServerAuthMethod::DevToken]); + if methods.is_empty() { + errors.push(ResolveError::Invalid { +- path: "server.auth.methods".to_string(), ++ path: "server.auth.methods".to_string(), + reason: "must not be empty".to_string(), + }); + } +@@ -136,7 +136,7 @@ fn resolve_auth( + .unwrap_or_default(); + if methods.contains(&ServerAuthMethod::Github) && github.allowed_usernames.is_empty() { + errors.push(ResolveError::Invalid { +- path: "server.auth.github.allowed_usernames".to_string(), ++ path: "server.auth.github.allowed_usernames".to_string(), + reason: "must not be empty when github auth is enabled".to_string(), + }); + } +@@ -162,7 +162,7 @@ fn resolve_artifacts( + prefix: layer + .and_then(|artifacts| artifacts.prefix.clone()) + .expect("defaults.toml should provide server.artifacts.prefix"), +- store: resolve_object_store( ++ store: resolve_object_store( + provider, + layer.and_then(|artifacts| artifacts.local.as_ref()), + layer.and_then(|artifacts| artifacts.s3.as_ref()), +@@ -257,16 +257,16 @@ fn object_store_default_root(storage_root: &InterpString, domain: &str) -> Inter + + fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegrationsSettings { + ServerIntegrationsSettings { +- github: layer ++ github: layer + .and_then(|integrations| integrations.github.as_ref()) + .map(|github| GithubIntegrationSettings { +- enabled: github.enabled.unwrap_or(true), +- strategy: github.strategy.unwrap_or_default(), +- app_id: github.app_id.clone(), +- client_id: github.client_id.clone(), +- slug: github.slug.clone(), ++ enabled: github.enabled.unwrap_or(true), ++ strategy: github.strategy.unwrap_or_default(), ++ app_id: github.app_id.clone(), ++ client_id: github.client_id.clone(), ++ slug: github.slug.clone(), + permissions: github.permissions.clone(), +- webhooks: github ++ webhooks: github + .webhooks + .as_ref() + .map(|webhooks| IntegrationWebhooksSettings { +@@ -274,10 +274,10 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr + }), + }) + .unwrap_or_default(), +- slack: layer ++ slack: layer + .and_then(|integrations| integrations.slack.as_ref()) + .map(|slack| SlackIntegrationSettings { +- enabled: slack.enabled.unwrap_or(true), ++ enabled: slack.enabled.unwrap_or(true), + default_channel: slack.default_channel.clone(), + }) + .unwrap_or_default(), +@@ -287,7 +287,7 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr + enabled: discord.enabled.unwrap_or(true), + }) + .unwrap_or_default(), +- teams: layer ++ teams: layer + .and_then(|integrations| integrations.teams.as_ref()) + .map(|teams| TeamsIntegrationSettings { + enabled: teams.enabled.unwrap_or(true), +diff --git a/lib/crates/fabro-config/src/resolve/workflow.rs b/lib/crates/fabro-config/src/resolve/workflow.rs +index dee80d1b..cbd9bff1 100644 +--- a/lib/crates/fabro-config/src/resolve/workflow.rs ++++ b/lib/crates/fabro-config/src/resolve/workflow.rs +@@ -7,12 +7,12 @@ pub fn resolve_workflow( + _errors: &mut Vec, + ) -> WorkflowSettings { + WorkflowSettings { +- name: layer.name.clone(), ++ name: layer.name.clone(), + description: layer.description.clone(), +- graph: layer ++ graph: layer + .graph + .clone() + .expect("defaults.toml should provide workflow.graph"), +- metadata: layer.metadata.clone(), ++ metadata: layer.metadata.clone(), + } + } +diff --git a/lib/crates/fabro-config/src/run.rs b/lib/crates/fabro-config/src/run.rs +index e4e53691..d6a6d93a 100644 +--- a/lib/crates/fabro-config/src/run.rs ++++ b/lib/crates/fabro-config/src/run.rs +@@ -43,7 +43,7 @@ pub enum ResolveRunGoalError { + var: String, + }, + Io { +- path: PathBuf, ++ path: PathBuf, + source: std::io::Error, + }, + } +@@ -81,7 +81,7 @@ pub fn resolve_run_goal( + + match goal { + RunGoalLayer::Inline(text) => Ok(Some(ResolvedRunGoal { +- text: text.as_source(), ++ text: text.as_source(), + source: ResolvedGoalSource::Inline, + })), + RunGoalLayer::File { file } => { +diff --git a/lib/crates/fabro-core/src/error.rs b/lib/crates/fabro-core/src/error.rs +index 1ba3a71f..f1877a10 100644 +--- a/lib/crates/fabro-core/src/error.rs ++++ b/lib/crates/fabro-core/src/error.rs +@@ -22,9 +22,9 @@ impl fmt::Display for VisitLimitSource { + /// to_fail_outcome(). + #[derive(Debug, Clone)] + pub struct HandlerErrorDetail { +- pub message: String, ++ pub message: String, + pub retryable: bool, +- pub category: Option, ++ pub category: Option, + pub signature: Option, + } + +@@ -48,9 +48,9 @@ pub enum Error { + "node \"{node_id}\" visited {visits} times ({limit_source} limit {limit}); run is stuck in a cycle" + )] + VisitLimitExceeded { +- node_id: String, +- visits: usize, +- limit: usize, ++ node_id: String, ++ visits: usize, ++ limit: usize, + limit_source: VisitLimitSource, + }, + #[error("stall timeout on node \"{node_id}\"")] +@@ -81,8 +81,8 @@ impl Error { + Self::Handler { detail } => Outcome { + status: StageStatus::Fail, + failure: Some(FailureDetail { +- message: detail.message.clone(), +- category: detail.category.unwrap_or(FailureCategory::Deterministic), ++ message: detail.message.clone(), ++ category: detail.category.unwrap_or(FailureCategory::Deterministic), + signature: detail.signature.clone(), + }), + ..Outcome::default() +@@ -118,9 +118,9 @@ mod tests { + ); + assert_eq!( + Error::VisitLimitExceeded { +- node_id: "n1".into(), +- visits: 5, +- limit: 3, ++ node_id: "n1".into(), ++ visits: 5, ++ limit: 3, + limit_source: VisitLimitSource::Node, + } + .to_string(), +@@ -142,17 +142,17 @@ mod tests { + #[test] + fn core_error_handler_is_retryable() { + let retryable = Error::handler(HandlerErrorDetail { +- message: "timeout".into(), ++ message: "timeout".into(), + retryable: true, +- category: None, ++ category: None, + signature: None, + }); + assert!(retryable.is_retryable()); + + let not_retryable = Error::handler(HandlerErrorDetail { +- message: "bad input".into(), ++ message: "bad input".into(), + retryable: false, +- category: None, ++ category: None, + signature: None, + }); + assert!(!not_retryable.is_retryable()); +@@ -162,9 +162,9 @@ mod tests { + fn core_error_handler_to_fail_outcome() { + use crate::outcome::FailureCategory; + let err = Error::handler(HandlerErrorDetail { +- message: "api down".into(), ++ message: "api down".into(), + retryable: true, +- category: Some(FailureCategory::TransientInfra), ++ category: Some(FailureCategory::TransientInfra), + signature: Some("sig123".into()), + }); + let outcome: Outcome = err.to_fail_outcome(); +diff --git a/lib/crates/fabro-core/src/executor.rs b/lib/crates/fabro-core/src/executor.rs +index 60053930..13148ce2 100644 +--- a/lib/crates/fabro-core/src/executor.rs ++++ b/lib/crates/fabro-core/src/executor.rs +@@ -18,15 +18,15 @@ use crate::state::ExecutionState; + + #[derive(Default)] + pub struct ExecutorOptions { +- pub cancel_token: Option>, +- pub stall_token: Option, ++ pub cancel_token: Option>, ++ pub stall_token: Option, + pub max_node_visits: Option, + } + + pub struct Executor { +- handler: Arc>, ++ handler: Arc>, + lifecycle: Box>, +- options: ExecutorOptions, ++ options: ExecutorOptions, + } + + enum NextStep { +@@ -37,9 +37,9 @@ enum NextStep { + } + + pub struct ExecutorBuilder { +- handler: Arc>, ++ handler: Arc>, + lifecycle: Option>>, +- options: ExecutorOptions, ++ options: ExecutorOptions, + } + + impl ExecutorBuilder { +@@ -77,9 +77,9 @@ impl ExecutorBuilder { + + pub fn build(self) -> Executor { + Executor { +- handler: self.handler, ++ handler: self.handler, + lifecycle: self.lifecycle.unwrap_or_else(|| Box::new(NoopLifecycle)), +- options: self.options, ++ options: self.options, + } + } + } +@@ -970,26 +970,26 @@ mod tests { + let handler = Arc::new( + CountingHandler::new(vec![ + Err(Error::handler(HandlerErrorDetail { +- message: "fail1".into(), ++ message: "fail1".into(), + retryable: true, +- category: None, ++ category: None, + signature: None, + })), + Err(Error::handler(HandlerErrorDetail { +- message: "fail2".into(), ++ message: "fail2".into(), + retryable: true, +- category: None, ++ category: None, + signature: None, + })), + Ok(Outcome::success()), + ]) + .with_retry_policy(RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(1), +- factor: 1.0, +- max_delay: Duration::from_millis(1), +- jitter: false, ++ factor: 1.0, ++ max_delay: Duration::from_millis(1), ++ jitter: false, + }, + }), + ); +@@ -1019,11 +1019,11 @@ mod tests { + ]) + .with_retry_policy(RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(1), +- factor: 1.0, +- max_delay: Duration::from_millis(1), +- jitter: false, ++ factor: 1.0, ++ max_delay: Duration::from_millis(1), ++ jitter: false, + }, + }), + ); +@@ -1041,9 +1041,9 @@ mod tests { + async fn executor_retry_non_retryable_error_no_retry() { + let handler = Arc::new( + CountingHandler::new(vec![Err(Error::handler(HandlerErrorDetail { +- message: "fatal".into(), ++ message: "fatal".into(), + retryable: false, +- category: None, ++ category: None, + signature: None, + }))]) + .with_retry_policy(RetryPolicy::with_max_attempts(3)), +@@ -1064,9 +1064,9 @@ mod tests { + // Default policy is RetryPolicy::none() (max_attempts=1) + let handler = Arc::new(CountingHandler::new(vec![Err(Error::handler( + HandlerErrorDetail { +- message: "fail".into(), ++ message: "fail".into(), + retryable: true, +- category: None, ++ category: None, + signature: None, + }, + ))])); +@@ -1099,11 +1099,11 @@ mod tests { + fn retry_policy(&self, _n: &TestNode, _g: &TestGraph) -> RetryPolicy { + RetryPolicy { + max_attempts: 2, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(1), +- factor: 1.0, +- max_delay: Duration::from_millis(1), +- jitter: false, ++ factor: 1.0, ++ max_delay: Duration::from_millis(1), ++ jitter: false, + }, + } + } +@@ -1143,20 +1143,20 @@ mod tests { + let handler = Arc::new( + CountingHandler::new(vec![ + Err(Error::handler(HandlerErrorDetail { +- message: "r".into(), ++ message: "r".into(), + retryable: true, +- category: None, ++ category: None, + signature: None, + })), + Ok(Outcome::success()), + ]) + .with_retry_policy(RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(1), +- factor: 1.0, +- max_delay: Duration::from_millis(1), +- jitter: false, ++ factor: 1.0, ++ max_delay: Duration::from_millis(1), ++ jitter: false, + }, + }), + ); +@@ -1187,20 +1187,20 @@ mod tests { + let handler = Arc::new( + CountingHandler::new(vec![ + Err(Error::handler(HandlerErrorDetail { +- message: "r".into(), ++ message: "r".into(), + retryable: true, +- category: None, ++ category: None, + signature: None, + })), + Ok(Outcome::success()), + ]) + .with_retry_policy(RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(1), +- factor: 1.0, +- max_delay: Duration::from_millis(1), +- jitter: false, ++ factor: 1.0, ++ max_delay: Duration::from_millis(1), ++ jitter: false, + }, + }), + ); +@@ -1237,20 +1237,20 @@ mod tests { + let handler = Arc::new( + CountingHandler::new(vec![ + Err(Error::handler(HandlerErrorDetail { +- message: "r".into(), ++ message: "r".into(), + retryable: true, +- category: None, ++ category: None, + signature: None, + })), + Ok(Outcome::success()), // should not be reached + ]) + .with_retry_policy(RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(1), +- factor: 1.0, +- max_delay: Duration::from_millis(1), +- jitter: false, ++ factor: 1.0, ++ max_delay: Duration::from_millis(1), ++ jitter: false, + }, + }), + ); +@@ -1278,11 +1278,11 @@ mod tests { + ]) + .with_retry_policy(RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_secs(5), +- factor: 2.0, +- max_delay: Duration::from_secs(60), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(60), ++ jitter: false, + }, + }), + ); +@@ -1619,10 +1619,13 @@ mod tests { + executor.run(&g, state).await.unwrap(); + let checkpoints = log.lock().unwrap().clone(); + // "start" checkpoints with next="work", "work" checkpoints with next="end" +- assert_eq!(checkpoints, vec![ +- ("start".to_string(), Some("work".to_string())), +- ("work".to_string(), Some("end".to_string())), +- ]); ++ assert_eq!( ++ checkpoints, ++ vec![ ++ ("start".to_string(), Some("work".to_string())), ++ ("work".to_string(), Some("end".to_string())), ++ ] ++ ); + } + + #[tokio::test] +@@ -1692,10 +1695,13 @@ mod tests { + + executor.run(&g, state).await.unwrap(); + +- assert_eq!(*log.lock().unwrap(), vec![ +- "after_record:start:start:hello".to_string(), +- "on_edge_selected:start:hello".to_string(), +- ]); ++ assert_eq!( ++ *log.lock().unwrap(), ++ vec![ ++ "after_record:start:start:hello".to_string(), ++ "on_edge_selected:start:hello".to_string(), ++ ] ++ ); + } + + #[tokio::test] +@@ -1744,10 +1750,10 @@ mod tests { + .lifecycle(Box::new(GateTracker(log2.clone()))) + .build(); + executor2.run(&g2, state2).await.unwrap(); +- assert_eq!(log2.lock().unwrap().clone(), vec![( +- "end".to_string(), +- false +- )]); ++ assert_eq!( ++ log2.lock().unwrap().clone(), ++ vec![("end".to_string(), false)] ++ ); + } + + #[tokio::test] +@@ -2036,9 +2042,9 @@ mod tests { + // First call: fail with retryable, then cancel stall during backoff + self.stall.cancel(); + Err(Error::handler(HandlerErrorDetail { +- message: "transient".into(), ++ message: "transient".into(), + retryable: true, +- category: None, ++ category: None, + signature: None, + })) + } else { +@@ -2048,11 +2054,11 @@ mod tests { + fn retry_policy(&self, _n: &TestNode, _g: &TestGraph) -> RetryPolicy { + RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_secs(60), +- factor: 1.0, +- max_delay: Duration::from_secs(60), +- jitter: false, ++ factor: 1.0, ++ max_delay: Duration::from_secs(60), ++ jitter: false, + }, + } + } +diff --git a/lib/crates/fabro-core/src/graph.rs b/lib/crates/fabro-core/src/graph.rs +index 304e266a..0cb1744b 100644 +--- a/lib/crates/fabro-core/src/graph.rs ++++ b/lib/crates/fabro-core/src/graph.rs +@@ -17,7 +17,7 @@ pub trait EdgeSpec: Send + Sync + Clone { + } + + pub struct EdgeSelection { +- pub edge: G::Edge, ++ pub edge: G::Edge, + pub reason: &'static str, + } + +diff --git a/lib/crates/fabro-core/src/lifecycle.rs b/lib/crates/fabro-core/src/lifecycle.rs +index 16d7ecf4..836925e3 100644 +--- a/lib/crates/fabro-core/src/lifecycle.rs ++++ b/lib/crates/fabro-core/src/lifecycle.rs +@@ -22,26 +22,26 @@ pub enum EdgeDecision { + } + + pub struct AttemptContext<'a, G: Graph> { +- pub node: &'a G::Node, +- pub attempt: u32, ++ pub node: &'a G::Node, ++ pub attempt: u32, + pub max_attempts: u32, + } + + pub struct AttemptResultContext<'a, G: Graph> { +- pub node: &'a G::Node, +- pub result: &'a NodeResult, +- pub attempt: u32, +- pub will_retry: bool, ++ pub node: &'a G::Node, ++ pub result: &'a NodeResult, ++ pub attempt: u32, ++ pub will_retry: bool, + pub backoff_delay: Option, + } + + pub struct EdgeContext<'a, G: Graph> { +- pub from: &'a str, +- pub to: &'a str, +- pub edge: Option, ++ pub from: &'a str, ++ pub to: &'a str, ++ pub edge: Option, + pub is_jump: bool, + pub outcome: &'a Outcome, +- pub reason: &'a str, ++ pub reason: &'a str, + } + + #[async_trait] +@@ -272,11 +272,11 @@ mod tests { + + /// A lifecycle that records which callbacks were called. + struct RecordingLifecycle { +- name: String, +- log: Arc>>, +- before_node_decision: Mutex>, ++ name: String, ++ log: Arc>>, ++ before_node_decision: Mutex>, + before_attempt_decision: Mutex>, +- edge_decision: Mutex>, ++ edge_decision: Mutex>, + } + + impl RecordingLifecycle { +@@ -525,8 +525,8 @@ mod tests { + let state = ExecutionState::new(&g).unwrap(); + let node = g.get_node("start").unwrap(); + let ctx = AttemptContext { +- node: &node, +- attempt: 1, ++ node: &node, ++ attempt: 1, + max_attempts: 1, + }; + let decision = lc.before_attempt(&ctx, &state).await.unwrap(); +@@ -549,8 +549,8 @@ mod tests { + let state = ExecutionState::new(&g).unwrap(); + let node = g.get_node("start").unwrap(); + let ctx = AttemptContext { +- node: &node, +- attempt: 1, ++ node: &node, ++ attempt: 1, + max_attempts: 1, + }; + let decision = lc.before_attempt(&ctx, &state).await.unwrap(); +@@ -569,10 +569,10 @@ mod tests { + let node = g.get_node("start").unwrap(); + let result = NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1); + let ctx = AttemptResultContext { +- node: &node, +- result: &result, +- attempt: 1, +- will_retry: false, ++ node: &node, ++ result: &result, ++ attempt: 1, ++ will_retry: false, + backoff_delay: None, + }; + lc.after_attempt(&ctx, &state).await.unwrap(); +@@ -595,12 +595,12 @@ mod tests { + let outcome = Outcome::success(); + let edge = g.outgoing_edges("start").into_iter().next().unwrap(); + let ctx = EdgeContext { +- from: "start", +- to: "end", +- edge: Some(edge), ++ from: "start", ++ to: "end", ++ edge: Some(edge), + is_jump: false, + outcome: &outcome, +- reason: "unconditional", ++ reason: "unconditional", + }; + let decision = lc.on_edge_selected(&ctx, &state).await.unwrap(); + assert!(matches!(decision, EdgeDecision::Override(ref t) if t == "other")); +@@ -622,12 +622,12 @@ mod tests { + let state = ExecutionState::new(&g).unwrap(); + let outcome = Outcome::success(); + let ctx = EdgeContext { +- from: "start", +- to: "end", +- edge: None, ++ from: "start", ++ to: "end", ++ edge: None, + is_jump: false, + outcome: &outcome, +- reason: "unconditional", ++ reason: "unconditional", + }; + let decision = lc.on_edge_selected(&ctx, &state).await.unwrap(); + assert!(matches!(decision, EdgeDecision::Block(_))); +@@ -641,12 +641,12 @@ mod tests { + let state = ExecutionState::new(&g).unwrap(); + let outcome = Outcome::success(); + let ctx = EdgeContext:: { +- from: "start", +- to: "target", +- edge: None, ++ from: "start", ++ to: "target", ++ edge: None, + is_jump: true, + outcome: &outcome, +- reason: "jump", ++ reason: "jump", + }; + let decision = lc.on_edge_selected(&ctx, &state).await.unwrap(); + assert!(matches!(decision, EdgeDecision::Continue)); +@@ -692,8 +692,8 @@ mod tests { + let counter = Arc::new(AtomicU32::new(0)); + + struct OrderedLifecycle { +- name: String, +- log: Arc>>, ++ name: String, ++ log: Arc>>, + counter: Arc, + } + +@@ -711,18 +711,18 @@ mod tests { + + let lc = CompositeLifecycle::new(vec![ + Box::new(OrderedLifecycle { +- name: "first".into(), +- log: log.clone(), ++ name: "first".into(), ++ log: log.clone(), + counter: counter.clone(), + }), + Box::new(OrderedLifecycle { +- name: "second".into(), +- log: log.clone(), ++ name: "second".into(), ++ log: log.clone(), + counter: counter.clone(), + }), + Box::new(OrderedLifecycle { +- name: "third".into(), +- log: log.clone(), ++ name: "third".into(), ++ log: log.clone(), + counter: counter.clone(), + }), + ]); +diff --git a/lib/crates/fabro-core/src/retry.rs b/lib/crates/fabro-core/src/retry.rs +index e16b413c..a13cf008 100644 +--- a/lib/crates/fabro-core/src/retry.rs ++++ b/lib/crates/fabro-core/src/retry.rs +@@ -3,14 +3,14 @@ pub use fabro_util::backoff::BackoffPolicy; + #[derive(Debug, Clone)] + pub struct RetryPolicy { + pub max_attempts: u32, +- pub backoff: BackoffPolicy, ++ pub backoff: BackoffPolicy, + } + + impl RetryPolicy { + pub fn none() -> Self { + Self { + max_attempts: 1, +- backoff: BackoffPolicy::default(), ++ backoff: BackoffPolicy::default(), + } + } + +diff --git a/lib/crates/fabro-core/src/stall.rs b/lib/crates/fabro-core/src/stall.rs +index 8068900b..10c7e8fe 100644 +--- a/lib/crates/fabro-core/src/stall.rs ++++ b/lib/crates/fabro-core/src/stall.rs +@@ -16,18 +16,18 @@ pub trait ActivityMonitor: Send + Sync { + /// Watches for inactivity and fires a stall timeout if no activity is + /// reported within the configured duration. + pub struct StallWatchdog { +- timeout: Duration, ++ timeout: Duration, + cancel_token: Arc, +- activity: Arc, +- shutdown: Arc, +- monitor: Arc, ++ activity: Arc, ++ shutdown: Arc, ++ monitor: Arc, + } + + /// Guard that resets the stall timer on activity. Drop to stop watching. + pub struct StallGuard { + activity: Arc, + shutdown: Arc, +- handle: Option>, ++ handle: Option>, + } + + impl StallWatchdog { +@@ -82,7 +82,7 @@ impl StallWatchdog { + StallGuard { + activity: self.activity, + shutdown: self.shutdown, +- handle: Some(handle), ++ handle: Some(handle), + } + } + } +diff --git a/lib/crates/fabro-core/src/state.rs b/lib/crates/fabro-core/src/state.rs +index 24dd1a0d..10617ceb 100644 +--- a/lib/crates/fabro-core/src/state.rs ++++ b/lib/crates/fabro-core/src/state.rs +@@ -17,30 +17,30 @@ impl std::fmt::Debug for ExecutionState { + } + + pub struct ExecutionState { +- pub context: Context, +- pub current_node_id: String, +- pub completed_nodes: Vec, +- pub node_outcomes: HashMap>, +- pub node_retries: HashMap, +- pub node_visits: HashMap, +- pub stage_index: usize, ++ pub context: Context, ++ pub current_node_id: String, ++ pub completed_nodes: Vec, ++ pub node_outcomes: HashMap>, ++ pub node_retries: HashMap, ++ pub node_visits: HashMap, ++ pub stage_index: usize, + pub previous_node_id: Option, +- pub cancelled: bool, ++ pub cancelled: bool, + } + + impl ExecutionState { + pub fn new(graph: &G) -> Result { + let start = graph.find_start_node()?; + Ok(Self { +- context: Context::new(), +- current_node_id: start.id().to_string(), +- completed_nodes: Vec::new(), +- node_outcomes: HashMap::new(), +- node_retries: HashMap::new(), +- node_visits: HashMap::new(), +- stage_index: 0, ++ context: Context::new(), ++ current_node_id: start.id().to_string(), ++ completed_nodes: Vec::new(), ++ node_outcomes: HashMap::new(), ++ node_retries: HashMap::new(), ++ node_visits: HashMap::new(), ++ stage_index: 0, + previous_node_id: None, +- cancelled: false, ++ cancelled: false, + }) + } + +diff --git a/lib/crates/fabro-core/src/test_fixtures.rs b/lib/crates/fabro-core/src/test_fixtures.rs +index dcb544b7..c653af92 100644 +--- a/lib/crates/fabro-core/src/test_fixtures.rs ++++ b/lib/crates/fabro-core/src/test_fixtures.rs +@@ -15,28 +15,28 @@ use crate::retry::RetryPolicy; + + #[derive(Debug, Clone)] + pub struct TestNode { +- pub id: String, +- pub terminal: bool, ++ pub id: String, ++ pub terminal: bool, + pub max_visits: Option, +- pub goal_gate: Option<(String, StageStatus)>, ++ pub goal_gate: Option<(String, StageStatus)>, + } + + impl TestNode { + pub fn new(id: &str) -> Self { + Self { +- id: id.to_string(), +- terminal: false, ++ id: id.to_string(), ++ terminal: false, + max_visits: None, +- goal_gate: None, ++ goal_gate: None, + } + } + + pub fn terminal(id: &str) -> Self { + Self { +- id: id.to_string(), +- terminal: true, ++ id: id.to_string(), ++ terminal: true, + max_visits: None, +- goal_gate: None, ++ goal_gate: None, + } + } + +@@ -71,18 +71,18 @@ impl NodeSpec for TestNode { + + #[derive(Debug, Clone)] + pub struct TestEdge { +- pub from: String, +- pub to: String, +- pub label: Option, ++ pub from: String, ++ pub to: String, ++ pub label: Option, + pub loop_restart: bool, + } + + impl TestEdge { + pub fn new(from: &str, to: &str) -> Self { + Self { +- from: from.to_string(), +- to: to.to_string(), +- label: None, ++ from: from.to_string(), ++ to: to.to_string(), ++ label: None, + loop_restart: false, + } + } +@@ -118,8 +118,8 @@ impl EdgeSpec for TestEdge { + + #[derive(Debug, Clone)] + pub struct TestGraph { +- pub nodes: Vec, +- pub edges: Vec, ++ pub nodes: Vec, ++ pub edges: Vec, + pub start_node_id: String, + pub retry_targets: HashMap, + } +@@ -180,7 +180,7 @@ impl Graph for TestGraph { + .find(|e| e.label.as_deref() == Some(label.as_str())) + { + return Some(EdgeSelection { +- edge: e.clone(), ++ edge: e.clone(), + reason: "preferred_label", + }); + } +@@ -193,7 +193,7 @@ impl Graph for TestGraph { + .find(|e| e.label.as_deref() == Some(status_label.as_str())) + { + return Some(EdgeSelection { +- edge: e.clone(), ++ edge: e.clone(), + reason: "condition", + }); + } +@@ -202,7 +202,7 @@ impl Graph for TestGraph { + for suggested in &outcome.suggested_next_ids { + if let Some(e) = edges.iter().find(|e| e.to == *suggested) { + return Some(EdgeSelection { +- edge: e.clone(), ++ edge: e.clone(), + reason: "suggested_next", + }); + } +@@ -211,7 +211,7 @@ impl Graph for TestGraph { + // Fourth: unconditional (no label) + if let Some(e) = edges.iter().find(|e| e.label.is_none()) { + return Some(EdgeSelection { +- edge: e.clone(), ++ edge: e.clone(), + reason: "unconditional", + }); + } +@@ -286,16 +286,16 @@ impl NodeHandler for AlwaysFailHandler { + } + + pub struct CountingHandler { +- pub call_count: AtomicU32, +- pub outcomes: std::sync::Mutex>>, ++ pub call_count: AtomicU32, ++ pub outcomes: std::sync::Mutex>>, + pub retry_policy: RetryPolicy, + } + + impl CountingHandler { + pub fn new(outcomes: Vec>) -> Self { + Self { +- call_count: AtomicU32::new(0), +- outcomes: std::sync::Mutex::new(outcomes), ++ call_count: AtomicU32::new(0), ++ outcomes: std::sync::Mutex::new(outcomes), + retry_policy: RetryPolicy::none(), + } + } +@@ -336,7 +336,7 @@ impl NodeHandler for CountingHandler { + /// A handler that dispatches based on node ID. + pub struct DispatchHandler { + handlers: HashMap>>, +- default: Arc>, ++ default: Arc>, + } + + impl DispatchHandler { +@@ -380,17 +380,17 @@ impl NodeHandler for DispatchHandler { + /// A handler that returns Err(Error::Handler) with configurable + /// retryability. + pub struct ErrorHandler { +- pub detail: HandlerErrorDetail, ++ pub detail: HandlerErrorDetail, + pub retry_policy: RetryPolicy, + } + + impl ErrorHandler { + pub fn retryable(message: &str, policy: RetryPolicy) -> Self { + Self { +- detail: HandlerErrorDetail { +- message: message.to_string(), ++ detail: HandlerErrorDetail { ++ message: message.to_string(), + retryable: true, +- category: None, ++ category: None, + signature: None, + }, + retry_policy: policy, +@@ -399,10 +399,10 @@ impl ErrorHandler { + + pub fn non_retryable(message: &str) -> Self { + Self { +- detail: HandlerErrorDetail { +- message: message.to_string(), ++ detail: HandlerErrorDetail { ++ message: message.to_string(), + retryable: false, +- category: None, ++ category: None, + signature: None, + }, + retry_policy: RetryPolicy::none(), +diff --git a/lib/crates/fabro-devcontainer/src/compose.rs b/lib/crates/fabro-devcontainer/src/compose.rs +index 6a29213a..06560423 100644 +--- a/lib/crates/fabro-devcontainer/src/compose.rs ++++ b/lib/crates/fabro-devcontainer/src/compose.rs +@@ -4,17 +4,17 @@ use std::path::{Path, PathBuf}; + /// Extracted configuration from a Docker Compose service. + #[derive(Debug, Clone, Default)] + pub(crate) struct ComposeServiceSpec { +- pub image: Option, +- pub build: Option, +- pub ports: Vec, ++ pub image: Option, ++ pub build: Option, ++ pub ports: Vec, + pub environment: HashMap, +- pub user: Option, ++ pub user: Option, + } + + /// Build configuration from a Docker Compose service. + #[derive(Debug, Clone)] + pub(crate) struct ComposeBuild { +- pub context: String, ++ pub context: String, + pub dockerfile: Option, + } + +@@ -62,7 +62,7 @@ fn parse_build(service: &serde_yaml::Value) -> Option { + + if let Some(context) = build_val.as_str() { + return Some(ComposeBuild { +- context: context.to_string(), ++ context: context.to_string(), + dockerfile: None, + }); + } +diff --git a/lib/crates/fabro-devcontainer/src/dockerfile.rs b/lib/crates/fabro-devcontainer/src/dockerfile.rs +index 24e11268..5f7a5725 100644 +--- a/lib/crates/fabro-devcontainer/src/dockerfile.rs ++++ b/lib/crates/fabro-devcontainer/src/dockerfile.rs +@@ -43,8 +43,8 @@ mod tests { + + fn make_layer(id: &str, dir_name: &str, snippet: &str) -> FeatureLayer { + FeatureLayer { +- id: id.to_string(), +- dir_name: dir_name.to_string(), ++ id: id.to_string(), ++ dir_name: dir_name.to_string(), + dockerfile_snippet: snippet.to_string(), + } + } +diff --git a/lib/crates/fabro-devcontainer/src/features.rs b/lib/crates/fabro-devcontainer/src/features.rs +index bd88e1ed..e7e33b52 100644 +--- a/lib/crates/fabro-devcontainer/src/features.rs ++++ b/lib/crates/fabro-devcontainer/src/features.rs +@@ -13,9 +13,9 @@ use crate::types::{FeatureMetadata, LifecycleCommand}; + #[derive(Debug, Clone)] + pub(crate) struct FeatureLayer { + /// Feature identifier (e.g. "ghcr.io/devcontainers/features/node:1") +- pub id: String, ++ pub id: String, + /// Directory name for COPY +- pub dir_name: String, ++ pub dir_name: String, + /// Dockerfile snippet for this feature + pub dockerfile_snippet: String, + } +@@ -23,11 +23,11 @@ pub(crate) struct FeatureLayer { + /// All resolved feature data: layers, environment, and lifecycle hooks. + #[derive(Debug, Clone, Default)] + pub(crate) struct ResolvedFeatures { +- pub layers: Vec, +- pub container_env: HashMap, +- pub on_create_commands: Vec, ++ pub layers: Vec, ++ pub container_env: HashMap, ++ pub on_create_commands: Vec, + pub post_create_commands: Vec, +- pub post_start_commands: Vec, ++ pub post_start_commands: Vec, + } + + /// Extract the directory name from a feature ID. +@@ -631,16 +631,16 @@ pub(crate) async fn resolve_features( + .get(id) + .cloned() + .unwrap_or_else(|| FeatureMetadata { +- id: None, +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, ++ id: None, ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, + post_create_command: None, +- post_start_command: None, ++ post_start_command: None, + }); + + // Collect feature containerEnv (later features override earlier) +@@ -757,18 +757,21 @@ mod tests { + let metadata: HashMap = ids + .iter() + .map(|id| { +- (id.clone(), FeatureMetadata { +- id: Some(id.clone()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }) ++ ( ++ id.clone(), ++ FeatureMetadata { ++ id: Some(id.clone()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ) + }) + .collect(); + +@@ -781,30 +784,36 @@ mod tests { + // A depends on B (A installs after B), so B should come first + let ids = vec!["a".to_string(), "b".to_string()]; + let mut metadata: HashMap = HashMap::new(); +- metadata.insert("a".to_string(), FeatureMetadata { +- id: Some("a".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: vec!["b".to_string()], +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); +- metadata.insert("b".to_string(), FeatureMetadata { +- id: Some("b".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); ++ metadata.insert( ++ "a".to_string(), ++ FeatureMetadata { ++ id: Some("a".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: vec!["b".to_string()], ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); ++ metadata.insert( ++ "b".to_string(), ++ FeatureMetadata { ++ id: Some("b".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); + + let sorted = topo_sort(&ids, &metadata); + assert_eq!(sorted, vec!["b", "a"]); +@@ -822,54 +831,66 @@ mod tests { + "a".to_string(), + ]; + let mut metadata: HashMap = HashMap::new(); +- metadata.insert("a".to_string(), FeatureMetadata { +- id: Some("a".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); +- metadata.insert("b".to_string(), FeatureMetadata { +- id: Some("b".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: vec!["a".to_string()], +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); +- metadata.insert("c".to_string(), FeatureMetadata { +- id: Some("c".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: vec!["a".to_string()], +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); +- metadata.insert("d".to_string(), FeatureMetadata { +- id: Some("d".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: vec!["b".to_string(), "c".to_string()], +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); ++ metadata.insert( ++ "a".to_string(), ++ FeatureMetadata { ++ id: Some("a".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); ++ metadata.insert( ++ "b".to_string(), ++ FeatureMetadata { ++ id: Some("b".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: vec!["a".to_string()], ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); ++ metadata.insert( ++ "c".to_string(), ++ FeatureMetadata { ++ id: Some("c".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: vec!["a".to_string()], ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); ++ metadata.insert( ++ "d".to_string(), ++ FeatureMetadata { ++ id: Some("d".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: vec!["b".to_string(), "c".to_string()], ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); + + let sorted = topo_sort(&ids, &metadata); + // A must come before B and C; B and C must come before D +@@ -887,22 +908,25 @@ mod tests { + fn generate_layer_with_options() { + let options = serde_json::json!({"version": "20"}); + let mut meta_options = HashMap::new(); +- meta_options.insert("version".to_string(), FeatureOption { +- option_type: Some("string".to_string()), +- default: Some(serde_json::Value::String("lts".to_string())), +- description: Some("Node.js version".to_string()), +- }); ++ meta_options.insert( ++ "version".to_string(), ++ FeatureOption { ++ option_type: Some("string".to_string()), ++ default: Some(serde_json::Value::String("lts".to_string())), ++ description: Some("Node.js version".to_string()), ++ }, ++ ); + let metadata = FeatureMetadata { +- id: Some("node".to_string()), +- name: Some("Node.js".to_string()), +- version: Some("1.0.0".to_string()), +- options: meta_options, +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, ++ id: Some("node".to_string()), ++ name: Some("Node.js".to_string()), ++ version: Some("1.0.0".to_string()), ++ options: meta_options, ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, + post_create_command: None, +- post_start_command: None, ++ post_start_command: None, + }; + + let snippet = generate_layer( +@@ -931,22 +955,25 @@ mod tests { + fn generate_layer_with_defaults() { + let options = serde_json::json!({}); + let mut meta_options = HashMap::new(); +- meta_options.insert("version".to_string(), FeatureOption { +- option_type: Some("string".to_string()), +- default: Some(serde_json::Value::String("lts".to_string())), +- description: Some("Node.js version".to_string()), +- }); ++ meta_options.insert( ++ "version".to_string(), ++ FeatureOption { ++ option_type: Some("string".to_string()), ++ default: Some(serde_json::Value::String("lts".to_string())), ++ description: Some("Node.js version".to_string()), ++ }, ++ ); + let metadata = FeatureMetadata { +- id: Some("node".to_string()), +- name: None, +- version: None, +- options: meta_options, +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, ++ id: Some("node".to_string()), ++ name: None, ++ version: None, ++ options: meta_options, ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, + post_create_command: None, +- post_start_command: None, ++ post_start_command: None, + }; + + let snippet = generate_layer( +@@ -975,16 +1002,16 @@ mod tests { + fn generate_layer_no_options() { + let options = serde_json::json!({}); + let metadata = FeatureMetadata { +- id: Some("common-utils".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, ++ id: Some("common-utils".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, + post_create_command: None, +- post_start_command: None, ++ post_start_command: None, + }; + + let snippet = generate_layer( +@@ -1015,30 +1042,36 @@ mod tests { + let mut metadata: HashMap = HashMap::new(); + let mut depends = HashMap::new(); + depends.insert("b".to_string(), serde_json::json!({})); +- metadata.insert("a".to_string(), FeatureMetadata { +- id: Some("a".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: depends, +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); +- metadata.insert("b".to_string(), FeatureMetadata { +- id: Some("b".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); ++ metadata.insert( ++ "a".to_string(), ++ FeatureMetadata { ++ id: Some("a".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: depends, ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); ++ metadata.insert( ++ "b".to_string(), ++ FeatureMetadata { ++ id: Some("b".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); + + let sorted = topo_sort(&ids, &metadata); + assert_eq!(sorted, vec!["b", "a"]); +@@ -1051,30 +1084,36 @@ mod tests { + let mut metadata: HashMap = HashMap::new(); + let mut depends = HashMap::new(); + depends.insert("b".to_string(), serde_json::json!({})); +- metadata.insert("a".to_string(), FeatureMetadata { +- id: Some("a".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: vec!["b".to_string()], +- depends_on: depends, +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); +- metadata.insert("b".to_string(), FeatureMetadata { +- id: Some("b".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, +- post_create_command: None, +- post_start_command: None, +- }); ++ metadata.insert( ++ "a".to_string(), ++ FeatureMetadata { ++ id: Some("a".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: vec!["b".to_string()], ++ depends_on: depends, ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); ++ metadata.insert( ++ "b".to_string(), ++ FeatureMetadata { ++ id: Some("b".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, ++ post_create_command: None, ++ post_start_command: None, ++ }, ++ ); + + let sorted = topo_sort(&ids, &metadata); + assert_eq!(sorted, vec!["b", "a"]); +@@ -1121,37 +1160,37 @@ mod tests { + let mut resolved = ResolvedFeatures::default(); + + let meta_a = FeatureMetadata { +- id: Some("a".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: { ++ id: Some("a".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: { + let mut env = HashMap::new(); + env.insert("FOO".to_string(), "from_a".to_string()); + env.insert("BAR".to_string(), "from_a".to_string()); + env + }, +- on_create_command: None, ++ on_create_command: None, + post_create_command: None, +- post_start_command: None, ++ post_start_command: None, + }; + let meta_b = FeatureMetadata { +- id: Some("b".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: { ++ id: Some("b".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: { + let mut env = HashMap::new(); + env.insert("FOO".to_string(), "from_b".to_string()); + env + }, +- on_create_command: None, ++ on_create_command: None, + post_create_command: None, +- post_start_command: None, ++ post_start_command: None, + }; + + // A is sorted first, then B — B's FOO overrides A's +@@ -1215,22 +1254,25 @@ mod tests { + fn generate_layer_shorthand_version() { + let options = serde_json::json!("20"); + let mut meta_options = HashMap::new(); +- meta_options.insert("version".to_string(), FeatureOption { +- option_type: Some("string".to_string()), +- default: Some(serde_json::Value::String("lts".to_string())), +- description: Some("Node.js version".to_string()), +- }); ++ meta_options.insert( ++ "version".to_string(), ++ FeatureOption { ++ option_type: Some("string".to_string()), ++ default: Some(serde_json::Value::String("lts".to_string())), ++ description: Some("Node.js version".to_string()), ++ }, ++ ); + let metadata = FeatureMetadata { +- id: Some("node".to_string()), +- name: None, +- version: None, +- options: meta_options, +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, ++ id: Some("node".to_string()), ++ name: None, ++ version: None, ++ options: meta_options, ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, + post_create_command: None, +- post_start_command: None, ++ post_start_command: None, + }; + + let snippet = generate_layer( +@@ -1248,16 +1290,16 @@ mod tests { + fn generate_layer_install_env_vars() { + let options = serde_json::json!({}); + let metadata = FeatureMetadata { +- id: Some("node".to_string()), +- name: None, +- version: None, +- options: HashMap::new(), +- installs_after: Vec::new(), +- depends_on: HashMap::new(), +- container_env: HashMap::new(), +- on_create_command: None, ++ id: Some("node".to_string()), ++ name: None, ++ version: None, ++ options: HashMap::new(), ++ installs_after: Vec::new(), ++ depends_on: HashMap::new(), ++ container_env: HashMap::new(), ++ on_create_command: None, + post_create_command: None, +- post_start_command: None, ++ post_start_command: None, + }; + + let snippet = generate_layer( +diff --git a/lib/crates/fabro-devcontainer/src/lib.rs b/lib/crates/fabro-devcontainer/src/lib.rs +index 036eeea0..832e197c 100644 +--- a/lib/crates/fabro-devcontainer/src/lib.rs ++++ b/lib/crates/fabro-devcontainer/src/lib.rs +@@ -26,33 +26,33 @@ pub enum Command { + #[derive(Debug, Clone)] + pub struct DevcontainerSpec { + /// Generated Dockerfile content +- pub dockerfile: String, ++ pub dockerfile: String, + /// Directory for docker build context +- pub build_context: PathBuf, ++ pub build_context: PathBuf, + /// Build arguments (docker build --build-arg) +- pub build_args: HashMap, ++ pub build_args: HashMap, + /// Multi-stage build target (docker build --target) +- pub build_target: Option, ++ pub build_target: Option, + /// Run on host before build +- pub initialize_commands: Vec, ++ pub initialize_commands: Vec, + /// Run in container after first creation (before updateContentCommand) +- pub on_create_commands: Vec, ++ pub on_create_commands: Vec, + /// Run in container after creation + pub post_create_commands: Vec, + /// Run in container on each start +- pub post_start_commands: Vec, ++ pub post_start_commands: Vec, + /// remoteEnv merged +- pub environment: HashMap, ++ pub environment: HashMap, + /// containerEnv — baked into Dockerfile as ENV directives +- pub container_env: HashMap, +- pub remote_user: Option, ++ pub container_env: HashMap, ++ pub remote_user: Option, + /// default: /workspaces/{repo-name} +- pub workspace_folder: String, ++ pub workspace_folder: String, + /// first = default preview port +- pub forwarded_ports: Vec, ++ pub forwarded_ports: Vec, + /// Compose file paths (empty if not in compose mode) +- pub compose_files: Vec, +- pub compose_service: Option, ++ pub compose_files: Vec, ++ pub compose_service: Option, + } + + #[derive(Debug, thiserror::Error)] +@@ -65,7 +65,7 @@ pub enum DevcontainerError { + + #[error("reading file {path}: {source}")] + ReadFile { +- path: PathBuf, ++ path: PathBuf, + source: std::io::Error, + }, + +diff --git a/lib/crates/fabro-devcontainer/src/types.rs b/lib/crates/fabro-devcontainer/src/types.rs +index 2e97d4e4..a881bdad 100644 +--- a/lib/crates/fabro-devcontainer/src/types.rs ++++ b/lib/crates/fabro-devcontainer/src/types.rs +@@ -110,8 +110,8 @@ pub enum LifecycleCommand { + #[derive(Debug, Clone, Deserialize)] + #[serde(rename_all = "camelCase")] + pub(crate) struct FeatureMetadata { +- pub id: Option, +- pub name: Option, ++ pub id: Option, ++ pub name: Option, + pub version: Option, + + #[serde(default)] +@@ -131,9 +131,9 @@ pub(crate) struct FeatureMetadata { + pub container_env: HashMap, + + /// Lifecycle hooks contributed by this feature +- pub on_create_command: Option, ++ pub on_create_command: Option, + pub post_create_command: Option, +- pub post_start_command: Option, ++ pub post_start_command: Option, + } + + /// A single option for a devcontainer feature. +@@ -141,7 +141,7 @@ pub(crate) struct FeatureMetadata { + pub(crate) struct FeatureOption { + #[serde(rename = "type")] + pub option_type: Option, +- pub default: Option, ++ pub default: Option, + pub description: Option, + } + +@@ -228,9 +228,10 @@ mod tests { + "workspaceFolder": "/workspace" + }"#; + let config: DevcontainerJson = serde_json::from_str(json).unwrap(); +- assert_eq!(config.docker_compose_file.as_ref().unwrap().paths(), vec![ +- "docker-compose.yml" +- ]); ++ assert_eq!( ++ config.docker_compose_file.as_ref().unwrap().paths(), ++ vec!["docker-compose.yml"] ++ ); + assert_eq!(config.service.as_deref(), Some("app")); + assert_eq!(config.workspace_folder.as_deref(), Some("/workspace")); + } +@@ -242,10 +243,10 @@ mod tests { + "service": "app" + }"#; + let config: DevcontainerJson = serde_json::from_str(json).unwrap(); +- assert_eq!(config.docker_compose_file.as_ref().unwrap().paths(), vec![ +- "docker-compose.yml", +- "docker-compose.override.yml" +- ]); ++ assert_eq!( ++ config.docker_compose_file.as_ref().unwrap().paths(), ++ vec!["docker-compose.yml", "docker-compose.override.yml"] ++ ); + } + + #[test] +diff --git a/lib/crates/fabro-github/src/lib.rs b/lib/crates/fabro-github/src/lib.rs +index e0f7035c..f074dbf0 100644 +--- a/lib/crates/fabro-github/src/lib.rs ++++ b/lib/crates/fabro-github/src/lib.rs +@@ -18,21 +18,21 @@ fn http_client() -> Result { + /// Detailed information about a pull request from the GitHub API. + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct PullRequestDetail { +- pub number: u64, +- pub title: String, +- pub body: Option, +- pub state: String, +- pub draft: bool, +- pub mergeable: Option, +- pub additions: u64, +- pub deletions: u64, ++ pub number: u64, ++ pub title: String, ++ pub body: Option, ++ pub state: String, ++ pub draft: bool, ++ pub mergeable: Option, ++ pub additions: u64, ++ pub deletions: u64, + pub changed_files: u64, +- pub html_url: String, +- pub user: PullRequestUser, +- pub head: PullRequestRef, +- pub base: PullRequestRef, +- pub created_at: String, +- pub updated_at: String, ++ pub html_url: String, ++ pub user: PullRequestUser, ++ pub head: PullRequestRef, ++ pub base: PullRequestRef, ++ pub created_at: String, ++ pub updated_at: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] +@@ -55,14 +55,14 @@ pub struct AppOwner { + /// Information about a GitHub App from the authenticated `/app` endpoint. + #[derive(Debug, Clone, Deserialize)] + pub struct AppInfo { +- pub slug: String, ++ pub slug: String, + pub owner: AppOwner, + } + + /// Credentials for authenticating as a GitHub App. + #[derive(Clone, Debug)] + pub struct GitHubAppCredentials { +- pub app_id: String, ++ pub app_id: String, + pub private_key_pem: String, + } + +@@ -173,7 +173,7 @@ pub enum HttpMethod { + /// A minimal HTTP response for testability. + pub struct HttpResponse { + pub status: u16, +- body: String, ++ body: String, + } + + impl HttpResponse { +@@ -453,8 +453,8 @@ pub async fn create_installation_access_token_for_pr( + /// Result of a successful pull request creation. + pub struct CreatedPullRequest { + pub html_url: String, +- pub number: u64, +- pub node_id: String, ++ pub number: u64, ++ pub node_id: String, + } + + /// Create a pull request on GitHub. +@@ -476,8 +476,8 @@ pub async fn create_pull_request( + #[derive(Deserialize)] + struct PullRequestResponse { + html_url: String, +- number: u64, +- node_id: String, ++ number: u64, ++ node_id: String, + } + + let client = http_client()?; +@@ -542,8 +542,8 @@ pub async fn create_pull_request( + + Ok(CreatedPullRequest { + html_url: pr.html_url, +- number: pr.number, +- node_id: pr.node_id, ++ number: pr.number, ++ node_id: pr.node_id, + }) + } + +@@ -1303,11 +1303,11 @@ mod tests { + // ----------------------------------------------------------------------- + + struct MockRoute { +- method: HttpMethod, +- path: String, +- status: u16, +- response_body: String, +- assert_header: Option<(String, MockHeaderCheck)>, ++ method: HttpMethod, ++ path: String, ++ status: u16, ++ response_body: String, ++ assert_header: Option<(String, MockHeaderCheck)>, + assert_body_json: Option, + } + +@@ -1554,7 +1554,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + let result = +@@ -1586,7 +1586,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + let result = +@@ -1618,7 +1618,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + let result = branch_exists_with_client(&mock, &creds, "owner", "repo", "broken", "").await; +@@ -1804,7 +1804,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + let detail = get_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "") +@@ -1841,7 +1841,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + let err = get_pull_request_with_client(&mock, &creds, "owner", "repo", 999, "") +@@ -1915,7 +1915,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") +@@ -1942,7 +1942,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + let err = merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") +@@ -1970,7 +1970,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + let err = merge_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "squash", "") +@@ -2007,7 +2007,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + close_pull_request_with_client(&mock, &creds, "owner", "repo", 42, "") +@@ -2034,7 +2034,7 @@ mod tests { + + let pem = test_rsa_key(); + let creds = GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test".to_string(), ++ app_id: "test".to_string(), + private_key_pem: pem.to_string(), + }); + let err = close_pull_request_with_client(&mock, &creds, "owner", "repo", 999, "") +diff --git a/lib/crates/fabro-github/tests/integration.rs b/lib/crates/fabro-github/tests/integration.rs +index 1943e4ce..e32be0e7 100644 +--- a/lib/crates/fabro-github/tests/integration.rs ++++ b/lib/crates/fabro-github/tests/integration.rs +@@ -9,7 +9,7 @@ const TEST_RSA_KEY: &str = include_str!("../src/testdata/rsa_private.pem"); + + fn github_credentials() -> GitHubCredentials { + GitHubCredentials::App(GitHubAppCredentials { +- app_id: "42".to_string(), ++ app_id: "42".to_string(), + private_key_pem: TEST_RSA_KEY.to_string(), + }) + } +@@ -17,12 +17,12 @@ fn github_credentials() -> GitHubCredentials { + fn standard_app_state() -> GitHubAppState { + let mut state = GitHubAppState::new(); + state.register_app(GitHubAppOptions { +- app_id: "42".into(), +- slug: "test-app".into(), +- owner_login: "acme".into(), +- public: true, ++ app_id: "42".into(), ++ slug: "test-app".into(), ++ owner_login: "acme".into(), ++ public: true, + private_key_pem: TEST_RSA_KEY.into(), +- webhook_secret: None, ++ webhook_secret: None, + }); + state.add_installation("42", "acme", vec!["widgets".into()], false); + state.add_repository( +diff --git a/lib/crates/fabro-graphviz/src/condition.rs b/lib/crates/fabro-graphviz/src/condition.rs +index 09e82eb9..b610dfd6 100644 +--- a/lib/crates/fabro-graphviz/src/condition.rs ++++ b/lib/crates/fabro-graphviz/src/condition.rs +@@ -28,8 +28,8 @@ pub enum ConditionExpr { + + #[derive(Debug, Clone, PartialEq)] + pub struct Clause { +- pub key: String, +- pub op: Op, ++ pub key: String, ++ pub op: Op, + pub value: String, + } + +@@ -207,7 +207,7 @@ fn is_word_operator_context(tokens: &[Token]) -> bool { + + struct Parser { + tokens: Vec, +- pos: usize, ++ pos: usize, + } + + impl Parser { +@@ -403,8 +403,8 @@ mod tests { + assert_eq!( + expr, + ConditionExpr::Clause(Clause { +- key: "outcome".to_string(), +- op: Op::Eq, ++ key: "outcome".to_string(), ++ op: Op::Eq, + value: "success".to_string(), + }) + ); +@@ -417,13 +417,13 @@ mod tests { + expr, + ConditionExpr::And(vec![ + ConditionExpr::Clause(Clause { +- key: "a".to_string(), +- op: Op::Eq, ++ key: "a".to_string(), ++ op: Op::Eq, + value: "1".to_string(), + }), + ConditionExpr::Clause(Clause { +- key: "b".to_string(), +- op: Op::Eq, ++ key: "b".to_string(), ++ op: Op::Eq, + value: "2".to_string(), + }), + ]) +@@ -436,8 +436,8 @@ mod tests { + assert_eq!( + expr, + ConditionExpr::Clause(Clause { +- key: "some_flag".to_string(), +- op: Op::Truthy, ++ key: "some_flag".to_string(), ++ op: Op::Truthy, + value: String::new(), + }) + ); +@@ -449,8 +449,8 @@ mod tests { + assert_eq!( + expr, + ConditionExpr::Clause(Clause { +- key: "outcome".to_string(), +- op: Op::NotEq, ++ key: "outcome".to_string(), ++ op: Op::NotEq, + value: "fail".to_string(), + }) + ); +@@ -529,8 +529,8 @@ mod tests { + assert_eq!( + expr, + ConditionExpr::Clause(Clause { +- key: "outcome".to_string(), +- op: Op::Eq, ++ key: "outcome".to_string(), ++ op: Op::Eq, + value: "success".to_string(), + }) + ); +@@ -549,8 +549,8 @@ mod tests { + assert_eq!( + expr, + ConditionExpr::Clause(Clause { +- key: "outcome".to_string(), +- op: Op::NotEq, ++ key: "outcome".to_string(), ++ op: Op::NotEq, + value: "fail".to_string(), + }) + ); +@@ -562,8 +562,8 @@ mod tests { + assert_eq!( + expr, + ConditionExpr::Clause(Clause { +- key: "context.msg".to_string(), +- op: Op::Eq, ++ key: "context.msg".to_string(), ++ op: Op::Eq, + value: "hello world".to_string(), + }) + ); +diff --git a/lib/crates/fabro-graphviz/src/parser/ast.rs b/lib/crates/fabro-graphviz/src/parser/ast.rs +index 8aa50e44..8ba32cda 100644 +--- a/lib/crates/fabro-graphviz/src/parser/ast.rs ++++ b/lib/crates/fabro-graphviz/src/parser/ast.rs +@@ -18,7 +18,7 @@ pub type AttrBlock = Vec<(String, AstValue)>; + /// A node statement: `id [attrs]?`. + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct NodeStmt { +- pub id: String, ++ pub id: String, + pub attrs: Option, + } + +@@ -33,7 +33,7 @@ pub struct EdgeStmt { + /// A subgraph statement: `subgraph name? { stmts }`. + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SubgraphStmt { +- pub name: Option, ++ pub name: Option, + pub statements: Vec, + } + +@@ -59,7 +59,7 @@ pub enum Statement { + /// The top-level parsed DOT graph. + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct DotGraph { +- pub name: String, ++ pub name: String, + pub statements: Vec, + } + +@@ -85,11 +85,11 @@ mod tests { + #[test] + fn dot_graph_construction() { + let graph = DotGraph { +- name: "test".into(), ++ name: "test".into(), + statements: vec![ + Statement::GraphAttrDecl("rankdir".into(), AstValue::Ident("LR".into())), + Statement::Node(NodeStmt { +- id: "start".into(), ++ id: "start".into(), + attrs: Some(vec![("shape".into(), AstValue::Ident("Mdiamond".into()))]), + }), + ], +@@ -110,7 +110,7 @@ mod tests { + #[test] + fn subgraph_stmt() { + let sub = SubgraphStmt { +- name: Some("cluster_loop".into()), ++ name: Some("cluster_loop".into()), + statements: vec![Statement::NodeDefaults(vec![( + "timeout".into(), + AstValue::Str("900s".into()), +diff --git a/lib/crates/fabro-graphviz/src/parser/grammar.rs b/lib/crates/fabro-graphviz/src/parser/grammar.rs +index 8cd4d1b8..cd37b182 100644 +--- a/lib/crates/fabro-graphviz/src/parser/grammar.rs ++++ b/lib/crates/fabro-graphviz/src/parser/grammar.rs +@@ -73,7 +73,7 @@ fn subgraph_stmt(input: &str) -> IResult<&str, Statement> { + Ok(( + rest, + Statement::Subgraph(SubgraphStmt { +- name: name.map(String::from), ++ name: name.map(String::from), + statements: stmts, + }), + )) +@@ -141,10 +141,13 @@ pub fn parse_dot_graph(input: &str) -> IResult<&str, DotGraph> { + let (rest, _) = preceded(ws, char('{'))(rest)?; + let (rest, stmts) = many0(statement)(rest)?; + let (rest, _) = preceded(ws, char('}'))(rest)?; +- Ok((rest, DotGraph { +- name: name.to_string(), +- statements: stmts, +- })) ++ Ok(( ++ rest, ++ DotGraph { ++ name: name.to_string(), ++ statements: stmts, ++ }, ++ )) + } + + // We need arrow to work with explicit error types +diff --git a/lib/crates/fabro-graphviz/src/parser/semantic.rs b/lib/crates/fabro-graphviz/src/parser/semantic.rs +index 64d7b695..eb82fa16 100644 +--- a/lib/crates/fabro-graphviz/src/parser/semantic.rs ++++ b/lib/crates/fabro-graphviz/src/parser/semantic.rs +@@ -58,7 +58,7 @@ fn derive_class_from_label(label: &str) -> String { + } + + struct SemanticState { +- graph: Graph, ++ graph: Graph, + node_defaults: HashMap, + edge_defaults: HashMap, + } +@@ -66,7 +66,7 @@ struct SemanticState { + impl SemanticState { + fn new(name: String) -> Self { + Self { +- graph: Graph::new(name), ++ graph: Graph::new(name), + node_defaults: HashMap::new(), + edge_defaults: HashMap::new(), + } +@@ -342,26 +342,26 @@ mod tests { + #[test] + fn ast_to_graph_simple_linear() { + let dot = DotGraph { +- name: "Simple".into(), ++ name: "Simple".into(), + statements: vec![ + Statement::GraphAttr(vec![("goal".into(), AstValue::Str("Run tests".into()))]), + Statement::GraphAttrDecl("rankdir".into(), AstValue::Ident("LR".into())), + Statement::Node(NodeStmt { +- id: "start".into(), ++ id: "start".into(), + attrs: Some(vec![ + ("shape".into(), AstValue::Ident("Mdiamond".into())), + ("label".into(), AstValue::Str("Start".into())), + ]), + }), + Statement::Node(NodeStmt { +- id: "exit".into(), ++ id: "exit".into(), + attrs: Some(vec![ + ("shape".into(), AstValue::Ident("Msquare".into())), + ("label".into(), AstValue::Str("Exit".into())), + ]), + }), + Statement::Node(NodeStmt { +- id: "run_tests".into(), ++ id: "run_tests".into(), + attrs: Some(vec![("label".into(), AstValue::Str("Run Tests".into()))]), + }), + Statement::Edge(EdgeStmt { +@@ -385,18 +385,18 @@ mod tests { + #[test] + fn ast_to_graph_node_defaults_applied() { + let dot = DotGraph { +- name: "Defaults".into(), ++ name: "Defaults".into(), + statements: vec![ + Statement::NodeDefaults(vec![ + ("shape".into(), AstValue::Ident("box".into())), + ("timeout".into(), AstValue::Str("900s".into())), + ]), + Statement::Node(NodeStmt { +- id: "plan".into(), ++ id: "plan".into(), + attrs: Some(vec![("label".into(), AstValue::Str("Plan".into()))]), + }), + Statement::Node(NodeStmt { +- id: "implement".into(), ++ id: "implement".into(), + attrs: Some(vec![ + ("label".into(), AstValue::Str("Implement".into())), + ("timeout".into(), AstValue::Str("1800s".into())), +@@ -429,13 +429,13 @@ mod tests { + #[test] + fn ast_to_graph_subgraph_class_derivation() { + let dot = DotGraph { +- name: "SubgraphTest".into(), ++ name: "SubgraphTest".into(), + statements: vec![Statement::Subgraph(SubgraphStmt { +- name: Some("cluster_loop".into()), ++ name: Some("cluster_loop".into()), + statements: vec![ + Statement::GraphAttrDecl("label".into(), AstValue::Str("Loop A".into())), + Statement::Node(NodeStmt { +- id: "plan".into(), ++ id: "plan".into(), + attrs: None, + }), + ], +@@ -450,16 +450,16 @@ mod tests { + #[test] + fn ast_to_graph_subgraph_class_from_graph_attr_block() { + let dot = DotGraph { +- name: "SubgraphAttrBlock".into(), ++ name: "SubgraphAttrBlock".into(), + statements: vec![Statement::Subgraph(SubgraphStmt { +- name: Some("cluster_review".into()), ++ name: Some("cluster_review".into()), + statements: vec![ + Statement::GraphAttr(vec![( + "label".into(), + AstValue::Str("Code Review".into()), + )]), + Statement::Node(NodeStmt { +- id: "reviewer".into(), ++ id: "reviewer".into(), + attrs: None, + }), + ], +@@ -474,7 +474,7 @@ mod tests { + #[test] + fn ast_to_graph_edge_defaults_applied() { + let dot = DotGraph { +- name: "EdgeDefaults".into(), ++ name: "EdgeDefaults".into(), + statements: vec![ + Statement::EdgeDefaults(vec![("weight".into(), AstValue::Int(5))]), + Statement::Edge(EdgeStmt { +@@ -491,7 +491,7 @@ mod tests { + #[test] + fn ast_to_graph_chained_edges_with_attrs() { + let dot = DotGraph { +- name: "Chained".into(), ++ name: "Chained".into(), + statements: vec![Statement::Edge(EdgeStmt { + nodes: vec!["a".into(), "b".into(), "c".into()], + attrs: Some(vec![("label".into(), AstValue::Str("next".into()))]), +@@ -507,9 +507,9 @@ mod tests { + #[test] + fn ast_to_graph_class_attr_parsed() { + let dot = DotGraph { +- name: "ClassTest".into(), ++ name: "ClassTest".into(), + statements: vec![Statement::Node(NodeStmt { +- id: "review".into(), ++ id: "review".into(), + attrs: Some(vec![( + "class".into(), + AstValue::Str("code,critical".into()), +@@ -526,7 +526,7 @@ mod tests { + #[test] + fn ast_to_graph_implicit_nodes_from_edges() { + let dot = DotGraph { +- name: "Implicit".into(), ++ name: "Implicit".into(), + statements: vec![Statement::Edge(EdgeStmt { + nodes: vec!["a".into(), "b".into()], + attrs: None, +@@ -541,17 +541,17 @@ mod tests { + #[test] + fn codergen_mode_legacy_translates_to_type() { + let dot = DotGraph { +- name: "Legacy".into(), ++ name: "Legacy".into(), + statements: vec![ + Statement::Node(NodeStmt { +- id: "classify".into(), ++ id: "classify".into(), + attrs: Some(vec![( + "codergen_mode".into(), + AstValue::Str("one_shot".into()), + )]), + }), + Statement::Node(NodeStmt { +- id: "work".into(), ++ id: "work".into(), + attrs: Some(vec![( + "codergen_mode".into(), + AstValue::Str("agent_loop".into()), +@@ -580,9 +580,9 @@ mod tests { + #[test] + fn codergen_mode_does_not_override_explicit_type() { + let dot = DotGraph { +- name: "ExplicitType".into(), ++ name: "ExplicitType".into(), + statements: vec![Statement::Node(NodeStmt { +- id: "gate".into(), ++ id: "gate".into(), + attrs: Some(vec![ + ("type".into(), AstValue::Str("human".into())), + ("codergen_mode".into(), AstValue::Str("one_shot".into())), +diff --git a/lib/crates/fabro-graphviz/src/stylesheet.rs b/lib/crates/fabro-graphviz/src/stylesheet.rs +index 5037cefd..4cbd9534 100644 +--- a/lib/crates/fabro-graphviz/src/stylesheet.rs ++++ b/lib/crates/fabro-graphviz/src/stylesheet.rs +@@ -29,13 +29,13 @@ impl Selector { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Declaration { + pub property: String, +- pub value: String, ++ pub value: String, + } + + /// A stylesheet rule: selector + declarations. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Rule { +- pub selector: Selector, ++ pub selector: Selector, + pub declarations: Vec, + } + +diff --git a/lib/crates/fabro-hooks/src/bridge.rs b/lib/crates/fabro-hooks/src/bridge.rs +index 0c51172a..08765a76 100644 +--- a/lib/crates/fabro-hooks/src/bridge.rs ++++ b/lib/crates/fabro-hooks/src/bridge.rs +@@ -12,12 +12,12 @@ use crate::types::{HookContext, HookDecision, HookEvent}; + /// Created per-node in the workflow engine, capturing the `HookRunner` and + /// context needed to build `HookContext` for tool-level events. + pub struct WorkflowToolHookCallback { +- pub hook_runner: Arc, +- pub sandbox: Arc, +- pub run_id: RunId, ++ pub hook_runner: Arc, ++ pub sandbox: Arc, ++ pub run_id: RunId, + pub workflow_name: String, +- pub work_dir: Option, +- pub node_id: String, ++ pub work_dir: Option, ++ pub node_id: String, + } + + impl WorkflowToolHookCallback { +@@ -84,7 +84,7 @@ mod tests { + + struct CapturingExecutor { + captured_contexts: Arc>>, +- decision: HookDecision, ++ decision: HookDecision, + } + + #[async_trait::async_trait] +@@ -98,8 +98,8 @@ mod tests { + ) -> HookResult { + self.captured_contexts.lock().unwrap().push(context.clone()); + HookResult { +- hook_name: None, +- decision: self.decision.clone(), ++ hook_name: None, ++ decision: self.decision.clone(), + duration_ms: 1, + } + } +@@ -143,7 +143,7 @@ mod tests { + let captured = Arc::new(Mutex::new(Vec::new())); + let executor = Arc::new(CapturingExecutor { + captured_contexts: captured.clone(), +- decision: HookDecision::Proceed, ++ decision: HookDecision::Proceed, + }); + let config = HookSettings { + hooks: vec![make_hook(HookEvent::PreToolUse)], +@@ -172,7 +172,7 @@ mod tests { + async fn pre_tool_use_maps_block_decision() { + let executor = Arc::new(CapturingExecutor { + captured_contexts: Arc::new(Mutex::new(Vec::new())), +- decision: HookDecision::Block { ++ decision: HookDecision::Block { + reason: Some("forbidden".into()), + }, + }); +@@ -184,16 +184,19 @@ mod tests { + let bridge = make_bridge(runner, sandbox); + + let decision = bridge.pre_tool_use("shell", &serde_json::json!({})).await; +- assert_eq!(decision, ToolHookDecision::Block { +- reason: "forbidden".to_string(), +- }); ++ assert_eq!( ++ decision, ++ ToolHookDecision::Block { ++ reason: "forbidden".to_string(), ++ } ++ ); + } + + #[tokio::test] + async fn pre_tool_use_maps_proceed() { + let executor = Arc::new(CapturingExecutor { + captured_contexts: Arc::new(Mutex::new(Vec::new())), +- decision: HookDecision::Proceed, ++ decision: HookDecision::Proceed, + }); + let config = HookSettings { + hooks: vec![make_hook(HookEvent::PreToolUse)], +@@ -211,7 +214,7 @@ mod tests { + let captured = Arc::new(Mutex::new(Vec::new())); + let executor = Arc::new(CapturingExecutor { + captured_contexts: captured.clone(), +- decision: HookDecision::Proceed, ++ decision: HookDecision::Proceed, + }); + let config = HookSettings { + hooks: vec![make_hook(HookEvent::PostToolUse)], +@@ -240,7 +243,7 @@ mod tests { + let captured = Arc::new(Mutex::new(Vec::new())); + let executor = Arc::new(CapturingExecutor { + captured_contexts: captured.clone(), +- decision: HookDecision::Proceed, ++ decision: HookDecision::Proceed, + }); + let config = HookSettings { + hooks: vec![make_hook(HookEvent::PostToolUseFailure)], +diff --git a/lib/crates/fabro-hooks/src/config.rs b/lib/crates/fabro-hooks/src/config.rs +index a6205c96..d20ef3bd 100644 +--- a/lib/crates/fabro-hooks/src/config.rs ++++ b/lib/crates/fabro-hooks/src/config.rs +@@ -94,20 +94,20 @@ pub enum HookType { + command: String, + }, + Http { +- url: String, +- headers: Option>, ++ url: String, ++ headers: Option>, + #[serde(default)] + allowed_env_vars: Vec, + #[serde(default)] +- tls: TlsMode, ++ tls: TlsMode, + }, + Prompt { + prompt: String, +- model: Option, ++ model: Option, + }, + Agent { +- prompt: String, +- model: Option, ++ prompt: String, ++ model: Option, + max_tool_rounds: Option, + }, + } +@@ -115,23 +115,23 @@ pub enum HookType { + /// A single hook definition. + #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] + pub struct HookDefinition { +- pub name: Option, +- pub event: HookEvent, ++ pub name: Option, ++ pub event: HookEvent, + /// Inline command shorthand — if set, implies `type = "command"`. + #[serde(default)] +- pub command: Option, ++ pub command: Option, + /// Explicit hook type (command or http). If omitted and `command` is set, + /// defaults to `Command`. + #[serde(flatten)] +- pub hook_type: Option, ++ pub hook_type: Option, + /// Regex matched against node_id, handler_type, or event-specific fields. +- pub matcher: Option, ++ pub matcher: Option, + /// Override the event's default blocking behavior. +- pub blocking: Option, ++ pub blocking: Option, + /// Timeout in milliseconds (default: 60_000). + pub timeout_ms: Option, + /// Run inside the sandbox (true, default) or on the host (false). +- pub sandbox: Option, ++ pub sandbox: Option, + } + + impl HookDefinition { +diff --git a/lib/crates/fabro-hooks/src/executor.rs b/lib/crates/fabro-hooks/src/executor.rs +index b7cba714..1e5fc8a9 100644 +--- a/lib/crates/fabro-hooks/src/executor.rs ++++ b/lib/crates/fabro-hooks/src/executor.rs +@@ -374,19 +374,19 @@ impl HookExecutorImpl { + + for _ in 0..rounds { + let request = Request { +- model: resolved_model.clone(), +- messages: messages.clone(), +- provider: None, +- tools: Some(tool_defs.clone()), +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: None, +- stop_sequences: None, ++ model: resolved_model.clone(), ++ messages: messages.clone(), ++ provider: None, ++ tools: Some(tool_defs.clone()), ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: None, ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + }; + +@@ -408,8 +408,8 @@ impl HookExecutorImpl { + for tc in &tool_calls { + let tool = registry.get(&tc.name).cloned(); + let ctx = ToolContext { +- env: sandbox.clone(), +- cancel: cancel.child_token(), ++ env: sandbox.clone(), ++ cancel: cancel.child_token(), + tool_env: None, + }; + let result = match tool { +@@ -560,17 +560,17 @@ impl HookExecutorImpl { + + /// Cached HTTP clients keyed by TLS mode. + struct HttpClientCache { +- verify: fabro_http::HttpClient, ++ verify: fabro_http::HttpClient, + no_verify: fabro_http::HttpClient, +- off: fabro_http::HttpClient, ++ off: fabro_http::HttpClient, + } + + impl HttpClientCache { + fn new() -> Self { + Self { +- verify: HookExecutorImpl::build_http_client(TlsMode::Verify), ++ verify: HookExecutorImpl::build_http_client(TlsMode::Verify), + no_verify: HookExecutorImpl::build_http_client(TlsMode::NoVerify), +- off: HookExecutorImpl::build_http_client(TlsMode::Off), ++ off: HookExecutorImpl::build_http_client(TlsMode::Off), + } + } + +@@ -710,14 +710,14 @@ mod tests { + + fn make_definition(command: &str) -> HookDefinition { + HookDefinition { +- name: Some("test-hook".into()), +- event: HookEvent::StageStart, +- command: Some(command.into()), +- hook_type: None, +- matcher: None, +- blocking: None, ++ name: Some("test-hook".into()), ++ event: HookEvent::StageStart, ++ command: Some(command.into()), ++ hook_type: None, ++ matcher: None, ++ blocking: None, + timeout_ms: Some(5000), +- sandbox: Some(false), // host execution for tests ++ sandbox: Some(false), // host execution for tests + } + } + +@@ -816,9 +816,12 @@ mod tests { + let ctx = make_context(); + let sandbox = make_sandbox(); + let result = executor.execute(&def, &ctx, sandbox, None).await; +- assert_eq!(result.decision, HookDecision::Skip { +- reason: Some("test skip".into()), +- }); ++ assert_eq!( ++ result.decision, ++ HookDecision::Skip { ++ reason: Some("test skip".into()), ++ } ++ ); + } + + #[tokio::test] +@@ -837,14 +840,14 @@ mod tests { + async fn no_hook_type_blocks() { + let executor = HookExecutorImpl; + let def = HookDefinition { +- name: None, +- event: HookEvent::StageStart, +- command: None, +- hook_type: None, +- matcher: None, +- blocking: None, ++ name: None, ++ event: HookEvent::StageStart, ++ command: None, ++ hook_type: None, ++ matcher: None, ++ blocking: None, + timeout_ms: None, +- sandbox: Some(false), ++ sandbox: Some(false), + }; + let ctx = make_context(); + let sandbox = make_sandbox(); +@@ -1006,9 +1009,12 @@ mod tests { + .await; + + mock.assert_async().await; +- assert_eq!(decision, HookDecision::Skip { +- reason: Some("not needed".into()), +- }); ++ assert_eq!( ++ decision, ++ HookDecision::Skip { ++ reason: Some("not needed".into()), ++ } ++ ); + } + + #[tokio::test] +@@ -1224,19 +1230,19 @@ mod tests { + + let executor = HookExecutorImpl; + let def = HookDefinition { +- name: Some("http-test".into()), +- event: HookEvent::StageStart, +- command: None, +- hook_type: Some(HookType::Http { +- url: server.url("/hook"), +- headers: None, ++ name: Some("http-test".into()), ++ event: HookEvent::StageStart, ++ command: None, ++ hook_type: Some(HookType::Http { ++ url: server.url("/hook"), ++ headers: None, + allowed_env_vars: vec![], +- tls: TlsMode::Off, ++ tls: TlsMode::Off, + }), +- matcher: None, +- blocking: None, ++ matcher: None, ++ blocking: None, + timeout_ms: Some(5000), +- sandbox: Some(false), ++ sandbox: Some(false), + }; + let ctx = make_context(); + let sandbox = make_sandbox(); +diff --git a/lib/crates/fabro-hooks/src/runner.rs b/lib/crates/fabro-hooks/src/runner.rs +index 5c617dca..3059c3f7 100644 +--- a/lib/crates/fabro-hooks/src/runner.rs ++++ b/lib/crates/fabro-hooks/src/runner.rs +@@ -11,8 +11,8 @@ use crate::types::{HookContext, HookDecision}; + /// Central orchestrator: filters matching hooks, executes them, merges + /// decisions. + pub struct HookRunner { +- config: HookSettings, +- executor: Arc, ++ config: HookSettings, ++ executor: Arc, + /// Pre-compiled regexes keyed by matcher pattern string. + compiled_matchers: HashMap, + } +@@ -231,8 +231,8 @@ mod tests { + _work_dir: Option<&Path>, + ) -> HookResult { + HookResult { +- hook_name: definition.name.clone(), +- decision: self.decision.clone(), ++ hook_name: definition.name.clone(), ++ decision: self.decision.clone(), + duration_ms: 1, + } + } +diff --git a/lib/crates/fabro-hooks/src/types.rs b/lib/crates/fabro-hooks/src/types.rs +index 28def4e7..5c7c8cfb 100644 +--- a/lib/crates/fabro-hooks/src/types.rs ++++ b/lib/crates/fabro-hooks/src/types.rs +@@ -6,41 +6,41 @@ pub use crate::config::HookEvent; + /// Rich JSON payload sent to hooks. + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct HookContext { +- pub event: HookEvent, +- pub run_id: RunId, +- pub workflow_name: String, ++ pub event: HookEvent, ++ pub run_id: RunId, ++ pub workflow_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub cwd: Option, ++ pub cwd: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub node_id: Option, ++ pub node_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub node_label: Option, ++ pub node_label: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub handler_type: Option, ++ pub handler_type: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub status: Option, ++ pub status: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub edge_from: Option, ++ pub edge_from: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub edge_to: Option, ++ pub edge_to: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub edge_label: Option, ++ pub edge_label: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub failure_reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub attempt: Option, ++ pub attempt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub max_attempts: Option, ++ pub max_attempts: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub tool_name: Option, ++ pub tool_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub tool_input: Option, ++ pub tool_input: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub tool_call_id: Option, ++ pub tool_call_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub tool_output: Option, ++ pub tool_output: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub error_message: Option, ++ pub error_message: Option, + } + + impl HookContext { +@@ -73,7 +73,7 @@ impl HookContext { + /// Response returned by prompt/agent hooks from the LLM. + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] + pub struct PromptHookResponse { +- pub ok: bool, ++ pub ok: bool, + #[serde(default)] + pub reason: Option, + } +@@ -119,8 +119,8 @@ impl HookDecision { + /// Result from executing a single hook. + #[derive(Debug, Clone)] + pub struct HookResult { +- pub hook_name: Option, +- pub decision: HookDecision, ++ pub hook_name: Option, ++ pub decision: HookDecision, + pub duration_ms: u64, + } + +@@ -133,25 +133,25 @@ mod tests { + #[test] + fn hook_context_serde_round_trip() { + let ctx = HookContext { +- event: HookEvent::StageStart, +- run_id: fixtures::RUN_1, +- workflow_name: "test-wf".into(), +- cwd: Some("/tmp".into()), +- node_id: Some("plan".into()), +- node_label: Some("Plan".into()), +- handler_type: Some("agent".into()), +- status: None, +- edge_from: None, +- edge_to: None, +- edge_label: None, ++ event: HookEvent::StageStart, ++ run_id: fixtures::RUN_1, ++ workflow_name: "test-wf".into(), ++ cwd: Some("/tmp".into()), ++ node_id: Some("plan".into()), ++ node_label: Some("Plan".into()), ++ handler_type: Some("agent".into()), ++ status: None, ++ edge_from: None, ++ edge_to: None, ++ edge_label: None, + failure_reason: None, +- attempt: Some(1), +- max_attempts: Some(3), +- tool_name: None, +- tool_input: None, +- tool_call_id: None, +- tool_output: None, +- error_message: None, ++ attempt: Some(1), ++ max_attempts: Some(3), ++ tool_name: None, ++ tool_input: None, ++ tool_call_id: None, ++ tool_output: None, ++ error_message: None, + }; + let json = serde_json::to_string(&ctx).unwrap(); + let back: HookContext = serde_json::from_str(&json).unwrap(); +diff --git a/lib/crates/fabro-http/src/lib.rs b/lib/crates/fabro-http/src/lib.rs +index f439b381..597d46c5 100644 +--- a/lib/crates/fabro-http/src/lib.rs ++++ b/lib/crates/fabro-http/src/lib.rs +@@ -65,7 +65,7 @@ macro_rules! define_builder { + ($builder_name:ident, $inner_builder:ty, $inner_new:expr, $client_type:ty) => { + #[derive(Default)] + pub struct $builder_name { +- inner: $inner_builder, ++ inner: $inner_builder, + proxy_policy: Option, + } + +@@ -73,7 +73,7 @@ macro_rules! define_builder { + #[must_use] + pub fn new() -> Self { + Self { +- inner: $inner_new, ++ inner: $inner_new, + proxy_policy: None, + } + } +@@ -214,7 +214,7 @@ mod tests { + use super::*; + + struct EnvGuard { +- key: &'static str, ++ key: &'static str, + original: Option, + } + +diff --git a/lib/crates/fabro-interview/src/auto_approve.rs b/lib/crates/fabro-interview/src/auto_approve.rs +index 8efb620f..947e77ab 100644 +--- a/lib/crates/fabro-interview/src/auto_approve.rs ++++ b/lib/crates/fabro-interview/src/auto_approve.rs +@@ -15,9 +15,9 @@ impl Interviewer for AutoApproveInterviewer { + question.options.first().map_or_else( + || Answer::text("auto-approved"), + |first| Answer { +- value: AnswerValue::Selected(first.key.clone()), ++ value: AnswerValue::Selected(first.key.clone()), + selected_option: Some(first.clone()), +- text: None, ++ text: None, + }, + ) + } +@@ -53,11 +53,11 @@ mod tests { + let mut q = Question::new("Choose:", QuestionType::MultipleChoice); + q.options = vec![ + QuestionOption { +- key: "A".to_string(), ++ key: "A".to_string(), + label: "Alpha".to_string(), + }, + QuestionOption { +- key: "B".to_string(), ++ key: "B".to_string(), + label: "Beta".to_string(), + }, + ]; +@@ -66,7 +66,7 @@ mod tests { + assert_eq!( + answer.selected_option, + Some(QuestionOption { +- key: "A".to_string(), ++ key: "A".to_string(), + label: "Alpha".to_string(), + }) + ); +diff --git a/lib/crates/fabro-interview/src/console.rs b/lib/crates/fabro-interview/src/console.rs +index e3217c3b..dddb4049 100644 +--- a/lib/crates/fabro-interview/src/console.rs ++++ b/lib/crates/fabro-interview/src/console.rs +@@ -34,9 +34,9 @@ fn find_matching_option(response: &str, options: &[QuestionOption]) -> Option Option= 1 && idx <= options.len() { + let opt = &options[idx - 1]; + return Some(Answer { +- value: AnswerValue::Selected(opt.key.clone()), ++ value: AnswerValue::Selected(opt.key.clone()), + selected_option: Some(opt.clone()), +- text: None, ++ text: None, + }); + } + } +@@ -146,9 +146,9 @@ fn ask_select_interactive(question: &Question) -> Answer { + Ok(Some(idx)) if idx < question.options.len() => { + let opt = &question.options[idx]; + Answer { +- value: AnswerValue::Selected(opt.key.clone()), ++ value: AnswerValue::Selected(opt.key.clone()), + selected_option: Some(opt.clone()), +- text: None, ++ text: None, + } + } + _ => Answer::interrupted(), +@@ -277,11 +277,11 @@ mod tests { + fn find_matching_option_by_key() { + let options = vec![ + crate::QuestionOption { +- key: "A".to_string(), ++ key: "A".to_string(), + label: "Approve".to_string(), + }, + crate::QuestionOption { +- key: "R".to_string(), ++ key: "R".to_string(), + label: "Reject".to_string(), + }, + ]; +@@ -294,7 +294,7 @@ mod tests { + #[test] + fn find_matching_option_by_key_case_insensitive() { + let options = vec![crate::QuestionOption { +- key: "Y".to_string(), ++ key: "Y".to_string(), + label: "Yes".to_string(), + }]; + let result = find_matching_option("y", &options); +@@ -305,11 +305,11 @@ mod tests { + fn find_matching_option_by_index() { + let options = vec![ + crate::QuestionOption { +- key: "A".to_string(), ++ key: "A".to_string(), + label: "Alpha".to_string(), + }, + crate::QuestionOption { +- key: "B".to_string(), ++ key: "B".to_string(), + label: "Beta".to_string(), + }, + ]; +@@ -322,7 +322,7 @@ mod tests { + #[test] + fn find_matching_option_no_match() { + let options = vec![crate::QuestionOption { +- key: "A".to_string(), ++ key: "A".to_string(), + label: "Alpha".to_string(), + }]; + let result = find_matching_option("zzz", &options); +@@ -332,7 +332,7 @@ mod tests { + #[test] + fn find_matching_option_index_out_of_range() { + let options = vec![crate::QuestionOption { +- key: "A".to_string(), ++ key: "A".to_string(), + label: "Alpha".to_string(), + }]; + let result = find_matching_option("5", &options); +@@ -343,7 +343,7 @@ mod tests { + fn non_tty_multiple_choice_eof_returns_interrupted() { + let mut question = Question::new("Approve?", QuestionType::MultipleChoice); + question.options = vec![crate::QuestionOption { +- key: "A".to_string(), ++ key: "A".to_string(), + label: "Approve".to_string(), + }]; + +diff --git a/lib/crates/fabro-interview/src/control.rs b/lib/crates/fabro-interview/src/control.rs +index 602c17d2..418e1dfe 100644 +--- a/lib/crates/fabro-interview/src/control.rs ++++ b/lib/crates/fabro-interview/src/control.rs +@@ -12,8 +12,8 @@ pub enum SubmitError { + + #[derive(Default)] + struct ControlInterviewerState { +- pending: HashMap>, +- queued: HashMap, ++ pending: HashMap>, ++ queued: HashMap, + terminal_answer: Option, + } + +diff --git a/lib/crates/fabro-interview/src/control_protocol.rs b/lib/crates/fabro-interview/src/control_protocol.rs +index 56284cb7..f62783c3 100644 +--- a/lib/crates/fabro-interview/src/control_protocol.rs ++++ b/lib/crates/fabro-interview/src/control_protocol.rs +@@ -6,7 +6,7 @@ pub const WORKER_CONTROL_PROTOCOL_VERSION: u8 = 1; + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct WorkerControlEnvelope { +- pub v: u8, ++ pub v: u8, + #[serde(flatten)] + pub message: WorkerControlMessage, + } +@@ -15,9 +15,9 @@ impl WorkerControlEnvelope { + #[must_use] + pub fn interview_answer(qid: impl Into, answer: Answer) -> Self { + Self { +- v: WORKER_CONTROL_PROTOCOL_VERSION, ++ v: WORKER_CONTROL_PROTOCOL_VERSION, + message: WorkerControlMessage::InterviewAnswer { +- qid: qid.into(), ++ qid: qid.into(), + answer: answer.into(), + }, + } +@@ -26,7 +26,7 @@ impl WorkerControlEnvelope { + #[must_use] + pub fn cancel_run() -> Self { + Self { +- v: WORKER_CONTROL_PROTOCOL_VERSION, ++ v: WORKER_CONTROL_PROTOCOL_VERSION, + message: WorkerControlMessage::RunCancel, + } + } +@@ -37,7 +37,7 @@ impl WorkerControlEnvelope { + pub enum WorkerControlMessage { + #[serde(rename = "interview.answer")] + InterviewAnswer { +- qid: String, ++ qid: String, + answer: WorkerControlAnswer, + }, + #[serde(rename = "run.cancel")] +@@ -84,9 +84,9 @@ impl From for Answer { + WorkerControlAnswer::Skipped => Self::skipped(), + WorkerControlAnswer::Timeout => Self::timeout(), + WorkerControlAnswer::Selected { key } => Self { +- value: AnswerValue::Selected(key), ++ value: AnswerValue::Selected(key), + selected_option: None, +- text: None, ++ text: None, + }, + WorkerControlAnswer::MultiSelected { keys } => Self::multi_selected(keys), + WorkerControlAnswer::Text { text } => Self::text(text), +diff --git a/lib/crates/fabro-interview/src/lib.rs b/lib/crates/fabro-interview/src/lib.rs +index c057b2fc..92965ce8 100644 +--- a/lib/crates/fabro-interview/src/lib.rs ++++ b/lib/crates/fabro-interview/src/lib.rs +@@ -38,7 +38,7 @@ impl std::fmt::Display for QuestionType { + /// An option presented to the user for multiple-choice questions. + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct QuestionOption { +- pub key: String, ++ pub key: String, + pub label: String, + } + +@@ -46,15 +46,15 @@ pub struct QuestionOption { + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Question { + #[serde(default)] +- pub id: String, +- pub text: String, +- pub question_type: QuestionType, +- pub options: Vec, +- pub allow_freeform: bool, +- pub default: Option, ++ pub id: String, ++ pub text: String, ++ pub question_type: QuestionType, ++ pub options: Vec, ++ pub allow_freeform: bool, ++ pub default: Option, + pub timeout_seconds: Option, +- pub stage: String, +- pub metadata: HashMap, ++ pub stage: String, ++ pub metadata: HashMap, + #[serde(default)] + pub context_display: Option, + } +@@ -93,89 +93,89 @@ pub enum AnswerValue { + /// An answer from the user. + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Answer { +- pub value: AnswerValue, ++ pub value: AnswerValue, + pub selected_option: Option, +- pub text: Option, ++ pub text: Option, + } + + impl Answer { + #[must_use] + pub fn yes() -> Self { + Self { +- value: AnswerValue::Yes, ++ value: AnswerValue::Yes, + selected_option: None, +- text: None, ++ text: None, + } + } + + #[must_use] + pub fn no() -> Self { + Self { +- value: AnswerValue::No, ++ value: AnswerValue::No, + selected_option: None, +- text: None, ++ text: None, + } + } + + #[must_use] + pub fn cancelled() -> Self { + Self { +- value: AnswerValue::Cancelled, ++ value: AnswerValue::Cancelled, + selected_option: None, +- text: None, ++ text: None, + } + } + + #[must_use] + pub fn interrupted() -> Self { + Self { +- value: AnswerValue::Interrupted, ++ value: AnswerValue::Interrupted, + selected_option: None, +- text: None, ++ text: None, + } + } + + #[must_use] + pub fn skipped() -> Self { + Self { +- value: AnswerValue::Skipped, ++ value: AnswerValue::Skipped, + selected_option: None, +- text: None, ++ text: None, + } + } + + #[must_use] + pub fn timeout() -> Self { + Self { +- value: AnswerValue::Timeout, ++ value: AnswerValue::Timeout, + selected_option: None, +- text: None, ++ text: None, + } + } + + pub fn selected(key: impl Into, option: QuestionOption) -> Self { + let key = key.into(); + Self { +- value: AnswerValue::Selected(key), ++ value: AnswerValue::Selected(key), + selected_option: Some(option), +- text: None, ++ text: None, + } + } + + pub fn multi_selected(keys: Vec) -> Self { + Self { +- value: AnswerValue::MultiSelected(keys), ++ value: AnswerValue::MultiSelected(keys), + selected_option: None, +- text: None, ++ text: None, + } + } + + pub fn text(text: impl Into) -> Self { + let t = text.into(); + Self { +- value: AnswerValue::Text(t.clone()), ++ value: AnswerValue::Text(t.clone()), + selected_option: None, +- text: Some(t), ++ text: Some(t), + } + } + } +@@ -301,7 +301,7 @@ mod tests { + #[test] + fn answer_selected() { + let opt = QuestionOption { +- key: "A".to_string(), ++ key: "A".to_string(), + label: "Approve".to_string(), + }; + let a = Answer::selected("A", opt.clone()); +@@ -319,11 +319,11 @@ mod tests { + #[test] + fn question_option_eq() { + let a = QuestionOption { +- key: "Y".to_string(), ++ key: "Y".to_string(), + label: "Yes".to_string(), + }; + let b = QuestionOption { +- key: "Y".to_string(), ++ key: "Y".to_string(), + label: "Yes".to_string(), + }; + assert_eq!(a, b); +diff --git a/lib/crates/fabro-interview/src/recording.rs b/lib/crates/fabro-interview/src/recording.rs +index 8c7ef8f2..18f1e366 100644 +--- a/lib/crates/fabro-interview/src/recording.rs ++++ b/lib/crates/fabro-interview/src/recording.rs +@@ -7,7 +7,7 @@ use crate::{Answer, Interviewer, Question}; + + /// Wraps another interviewer and records all question-answer pairs. + pub struct RecordingInterviewer { +- inner: Box, ++ inner: Box, + recordings: Mutex>, + } + +diff --git a/lib/crates/fabro-llm/src/client.rs b/lib/crates/fabro-llm/src/client.rs +index f589c47b..0321948c 100644 +--- a/lib/crates/fabro-llm/src/client.rs ++++ b/lib/crates/fabro-llm/src/client.rs +@@ -18,9 +18,9 @@ const INCEPTION_BASE_URL: &str = "https://api.inceptionlabs.ai/v1"; + /// The core client that routes requests to provider adapters (Section 2.2, 3). + #[derive(Clone)] + pub struct Client { +- providers: HashMap>, ++ providers: HashMap>, + default_provider: Option, +- middleware: Vec>, ++ middleware: Vec>, + } + + impl Client { +@@ -49,16 +49,16 @@ impl Client { + let mut credentials = Vec::new(); + if let Ok(key) = std::env::var("ANTHROPIC_API_KEY") { + credentials.push(ApiCredential { +- provider: fabro_model::Provider::Anthropic, +- auth_header: ApiKeyHeader::Custom { +- name: "x-api-key".to_string(), ++ provider: fabro_model::Provider::Anthropic, ++ auth_header: ApiKeyHeader::Custom { ++ name: "x-api-key".to_string(), + value: key, + }, + extra_headers: HashMap::new(), +- base_url: std::env::var("ANTHROPIC_BASE_URL").ok(), +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: std::env::var("ANTHROPIC_BASE_URL").ok(), ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }); + } + if let Ok(key) = std::env::var("OPENAI_API_KEY") { +@@ -85,57 +85,57 @@ impl Client { + std::env::var("GEMINI_API_KEY").or_else(|_| std::env::var("GOOGLE_API_KEY")) + { + credentials.push(ApiCredential { +- provider: fabro_model::Provider::Gemini, +- auth_header: ApiKeyHeader::Bearer(key), ++ provider: fabro_model::Provider::Gemini, ++ auth_header: ApiKeyHeader::Bearer(key), + extra_headers: HashMap::new(), +- base_url: std::env::var("GEMINI_BASE_URL").ok(), +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: std::env::var("GEMINI_BASE_URL").ok(), ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }); + } + if let Ok(key) = std::env::var("KIMI_API_KEY") { + credentials.push(ApiCredential { +- provider: fabro_model::Provider::Kimi, +- auth_header: ApiKeyHeader::Bearer(key), ++ provider: fabro_model::Provider::Kimi, ++ auth_header: ApiKeyHeader::Bearer(key), + extra_headers: HashMap::new(), +- base_url: None, +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: None, ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }); + } + if let Ok(key) = std::env::var("ZAI_API_KEY") { + credentials.push(ApiCredential { +- provider: fabro_model::Provider::Zai, +- auth_header: ApiKeyHeader::Bearer(key), ++ provider: fabro_model::Provider::Zai, ++ auth_header: ApiKeyHeader::Bearer(key), + extra_headers: HashMap::new(), +- base_url: None, +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: None, ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }); + } + if let Ok(key) = std::env::var("MINIMAX_API_KEY") { + credentials.push(ApiCredential { +- provider: fabro_model::Provider::Minimax, +- auth_header: ApiKeyHeader::Bearer(key), ++ provider: fabro_model::Provider::Minimax, ++ auth_header: ApiKeyHeader::Bearer(key), + extra_headers: HashMap::new(), +- base_url: None, +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: None, ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }); + } + if let Ok(key) = std::env::var("INCEPTION_API_KEY") { + credentials.push(ApiCredential { +- provider: fabro_model::Provider::Inception, +- auth_header: ApiKeyHeader::Bearer(key), ++ provider: fabro_model::Provider::Inception, ++ auth_header: ApiKeyHeader::Bearer(key), + extra_headers: HashMap::new(), +- base_url: None, +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: None, ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }); + } + Self::from_credentials(credentials).await +@@ -148,9 +148,9 @@ impl Client { + /// Returns `Error` if any provider adapter fails to initialize. + pub async fn from_credentials(credentials: Vec) -> Result { + let mut client = Self { +- providers: HashMap::new(), ++ providers: HashMap::new(), + default_provider: None, +- middleware: Vec::new(), ++ middleware: Vec::new(), + }; + + for credential in credentials { +@@ -251,7 +251,7 @@ impl Client { + return Err(Error::Configuration { + message: "Provider::OpenAiCompatible is not supported by from_credentials" + .to_string(), +- source: None, ++ source: None, + }); + } + } +@@ -304,7 +304,7 @@ impl Client { + .or(self.default_provider.as_deref()) + .ok_or_else(|| Error::Configuration { + message: "No provider specified and no default provider set".into(), +- source: None, ++ source: None, + })?; + + self.providers +@@ -312,7 +312,7 @@ impl Client { + .cloned() + .ok_or_else(|| Error::Configuration { + message: format!("Provider '{provider_name}' not registered"), +- source: None, ++ source: None, + }) + } + +@@ -446,19 +446,19 @@ mod tests { + + async fn complete(&self, _request: &Request) -> Result { + Ok(Response { +- id: "resp_mock".into(), +- model: "mock-model".into(), +- provider: self.provider_name.clone(), +- message: Message::assistant(&self.response_text), ++ id: "resp_mock".into(), ++ model: "mock-model".into(), ++ provider: self.provider_name.clone(), ++ message: Message::assistant(&self.response_text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 20, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -489,19 +489,19 @@ mod tests { + + fn test_request() -> Request { + Request { +- model: "mock-model".into(), +- messages: vec![Message::user("Hello")], +- provider: None, +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: None, +- stop_sequences: None, ++ model: "mock-model".into(), ++ messages: vec![Message::user("Hello")], ++ provider: None, ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: None, ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + } + } +@@ -564,25 +564,25 @@ mod tests { + async fn from_credentials_registers_multiple_providers() { + let client = Client::from_credentials(vec![ + ApiCredential { +- provider: fabro_model::Provider::Anthropic, +- auth_header: ApiKeyHeader::Custom { +- name: "x-api-key".to_string(), ++ provider: fabro_model::Provider::Anthropic, ++ auth_header: ApiKeyHeader::Custom { ++ name: "x-api-key".to_string(), + value: "anthropic-key".to_string(), + }, + extra_headers: HashMap::new(), +- base_url: None, +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: None, ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }, + ApiCredential { +- provider: fabro_model::Provider::OpenAi, +- auth_header: ApiKeyHeader::Bearer("openai-key".to_string()), ++ provider: fabro_model::Provider::OpenAi, ++ auth_header: ApiKeyHeader::Bearer("openai-key".to_string()), + extra_headers: HashMap::new(), +- base_url: None, +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: None, ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }, + ]) + .await +@@ -597,13 +597,13 @@ mod tests { + #[tokio::test] + async fn from_credentials_supports_openai_compatible_provider_constants() { + let client = Client::from_credentials(vec![ApiCredential { +- provider: fabro_model::Provider::Kimi, +- auth_header: ApiKeyHeader::Bearer("kimi-key".to_string()), ++ provider: fabro_model::Provider::Kimi, ++ auth_header: ApiKeyHeader::Bearer("kimi-key".to_string()), + extra_headers: HashMap::new(), +- base_url: None, +- codex_mode: false, +- org_id: None, +- project_id: None, ++ base_url: None, ++ codex_mode: false, ++ org_id: None, ++ project_id: None, + }]) + .await + .unwrap(); +diff --git a/lib/crates/fabro-llm/src/error.rs b/lib/crates/fabro-llm/src/error.rs +index fd21dd49..876fca13 100644 +--- a/lib/crates/fabro-llm/src/error.rs ++++ b/lib/crates/fabro-llm/src/error.rs +@@ -30,23 +30,23 @@ impl std::fmt::Display for ProviderErrorKind { + + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + pub struct ProviderErrorDetail { +- pub message: String, +- pub provider: String, ++ pub message: String, ++ pub provider: String, + pub status_code: Option, +- pub error_code: Option, ++ pub error_code: Option, + pub retry_after: Option, +- pub raw: Option, ++ pub raw: Option, + } + + impl ProviderErrorDetail { + pub fn new(message: impl Into, provider: impl Into) -> Self { + Self { +- message: message.into(), +- provider: provider.into(), ++ message: message.into(), ++ provider: provider.into(), + status_code: None, +- error_code: None, ++ error_code: None, + retry_after: None, +- raw: None, ++ raw: None, + } + } + } +@@ -58,7 +58,7 @@ use std::sync::Arc; + pub enum Error { + #[error("{kind} {}: {}", .detail.provider, .detail.message)] + Provider { +- kind: ProviderErrorKind, ++ kind: ProviderErrorKind, + detail: Box, + }, + +@@ -67,7 +67,7 @@ pub enum Error { + message: String, + #[source] + #[serde(skip)] +- source: Option>, ++ source: Option>, + }, + + #[error("Request interrupted: {message}")] +@@ -78,7 +78,7 @@ pub enum Error { + message: String, + #[source] + #[serde(skip)] +- source: Option>, ++ source: Option>, + }, + + #[error("Stream error: {message}")] +@@ -86,7 +86,7 @@ pub enum Error { + message: String, + #[source] + #[serde(skip)] +- source: Option>, ++ source: Option>, + }, + + #[error("Invalid tool call: {message}")] +@@ -100,7 +100,7 @@ pub enum Error { + message: String, + #[source] + #[serde(skip)] +- source: Option>, ++ source: Option>, + }, + + #[error("Unsupported tool choice: {message}")] +@@ -114,7 +114,7 @@ impl Error { + ) -> Self { + Self::Network { + message: message.into(), +- source: Some(Arc::new(source)), ++ source: Some(Arc::new(source)), + } + } + +@@ -124,7 +124,7 @@ impl Error { + ) -> Self { + Self::RequestTimeout { + message: message.into(), +- source: Some(Arc::new(source)), ++ source: Some(Arc::new(source)), + } + } + +@@ -134,7 +134,7 @@ impl Error { + ) -> Self { + Self::Stream { + message: message.into(), +- source: Some(Arc::new(source)), ++ source: Some(Arc::new(source)), + } + } + +@@ -144,7 +144,7 @@ impl Error { + ) -> Self { + Self::Configuration { + message: message.into(), +- source: Some(Arc::new(source)), ++ source: Some(Arc::new(source)), + } + } + +@@ -291,7 +291,7 @@ pub fn error_from_status_code( + 408 => { + return Error::RequestTimeout { + message: detail.message, +- source: None, ++ source: None, + }; + } + 413 => ProviderErrorKind::ContextLength, +@@ -349,7 +349,7 @@ pub fn error_from_grpc_status( + "DEADLINE_EXCEEDED" => { + return Error::RequestTimeout { + message: detail.message, +- source: None, ++ source: None, + }; + } + _ => ProviderErrorKind::Server, +@@ -372,7 +372,7 @@ mod tests { + #[test] + fn retryable_classification() { + let auth_err = Error::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail { + status_code: Some(401), + ..ProviderErrorDetail::new("bad key", "openai") +@@ -381,7 +381,7 @@ mod tests { + assert!(!auth_err.retryable()); + + let rate_err = Error::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail { + status_code: Some(429), + retry_after: Some(2.0), +@@ -392,7 +392,7 @@ mod tests { + assert_eq!(rate_err.retry_after(), Some(2.0)); + + let server_err = Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail { + status_code: Some(500), + ..ProviderErrorDetail::new("internal error", "anthropic") +@@ -402,19 +402,19 @@ mod tests { + + let timeout = Error::RequestTimeout { + message: "timed out".into(), +- source: None, ++ source: None, + }; + assert!(!timeout.retryable()); + + let network = Error::Network { + message: "connection refused".into(), +- source: None, ++ source: None, + }; + assert!(network.retryable()); + + let config = Error::Configuration { + message: "missing provider".into(), +- source: None, ++ source: None, + }; + assert!(!config.retryable()); + } +@@ -424,37 +424,37 @@ mod tests { + let detail = || Box::new(ProviderErrorDetail::new("error", "openai")); + + let access_denied = Error::Provider { +- kind: ProviderErrorKind::AccessDenied, ++ kind: ProviderErrorKind::AccessDenied, + detail: detail(), + }; + assert!(!access_denied.retryable()); + + let not_found = Error::Provider { +- kind: ProviderErrorKind::NotFound, ++ kind: ProviderErrorKind::NotFound, + detail: detail(), + }; + assert!(!not_found.retryable()); + + let invalid_req = Error::Provider { +- kind: ProviderErrorKind::InvalidRequest, ++ kind: ProviderErrorKind::InvalidRequest, + detail: detail(), + }; + assert!(!invalid_req.retryable()); + + let ctx_length = Error::Provider { +- kind: ProviderErrorKind::ContextLength, ++ kind: ProviderErrorKind::ContextLength, + detail: detail(), + }; + assert!(!ctx_length.retryable()); + + let quota = Error::Provider { +- kind: ProviderErrorKind::QuotaExceeded, ++ kind: ProviderErrorKind::QuotaExceeded, + detail: detail(), + }; + assert!(!quota.retryable()); + + let content_filter = Error::Provider { +- kind: ProviderErrorKind::ContentFilter, ++ kind: ProviderErrorKind::ContentFilter, + detail: detail(), + }; + assert!(!content_filter.retryable()); +@@ -488,32 +488,44 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Authentication, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Authentication, ++ .. ++ } ++ )); + assert!(!err.retryable()); + + let err = + error_from_status_code(403, "forbidden".into(), "openai".into(), None, None, None); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::AccessDenied, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::AccessDenied, ++ .. ++ } ++ )); + + let err = + error_from_status_code(404, "not found".into(), "openai".into(), None, None, None); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::NotFound, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::NotFound, ++ .. ++ } ++ )); + + let err = + error_from_status_code(400, "bad request".into(), "openai".into(), None, None, None); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::InvalidRequest, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::InvalidRequest, ++ .. ++ } ++ )); + + let err = error_from_status_code( + 422, +@@ -523,20 +535,26 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::InvalidRequest, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::InvalidRequest, ++ .. ++ } ++ )); + + let err = error_from_status_code(408, "timeout".into(), "openai".into(), None, None, None); + assert!(matches!(err, Error::RequestTimeout { .. })); + + let err = + error_from_status_code(413, "too large".into(), "openai".into(), None, None, None); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::ContextLength, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::ContextLength, ++ .. ++ } ++ )); + + let err = error_from_status_code( + 429, +@@ -546,26 +564,35 @@ mod tests { + None, + Some(5.0), + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::RateLimit, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::RateLimit, ++ .. ++ } ++ )); + assert!(err.retryable()); + assert_eq!(err.retry_after(), Some(5.0)); + + let err = error_from_status_code(500, "internal".into(), "openai".into(), None, None, None); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Server, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Server, ++ .. ++ } ++ )); + assert!(err.retryable()); + + let err = + error_from_status_code(502, "bad gateway".into(), "openai".into(), None, None, None); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Server, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Server, ++ .. ++ } ++ )); + + let err = error_from_status_code( + 529, +@@ -575,10 +602,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Server, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Server, ++ .. ++ } ++ )); + assert!(err.retryable()); + } + +@@ -592,10 +622,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::ContextLength, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::ContextLength, ++ .. ++ } ++ )); + } + + #[test] +@@ -608,10 +641,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::ContextLength, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::ContextLength, ++ .. ++ } ++ )); + } + + #[test] +@@ -624,10 +660,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::ContentFilter, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::ContentFilter, ++ .. ++ } ++ )); + } + + #[test] +@@ -640,10 +679,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::ContentFilter, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::ContentFilter, ++ .. ++ } ++ )); + } + + #[test] +@@ -656,10 +698,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::NotFound, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::NotFound, ++ .. ++ } ++ )); + } + + #[test] +@@ -672,10 +717,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::NotFound, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::NotFound, ++ .. ++ } ++ )); + } + + #[test] +@@ -688,10 +736,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Authentication, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Authentication, ++ .. ++ } ++ )); + } + + #[test] +@@ -704,10 +755,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Authentication, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Authentication, ++ .. ++ } ++ )); + } + + #[test] +@@ -720,10 +774,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::NotFound, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::NotFound, ++ .. ++ } ++ )); + + let err = error_from_grpc_status( + "RESOURCE_EXHAUSTED", +@@ -733,10 +790,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::RateLimit, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::RateLimit, ++ .. ++ } ++ )); + assert!(err.retryable()); + + let err = error_from_grpc_status( +@@ -747,10 +807,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Authentication, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Authentication, ++ .. ++ } ++ )); + + let err = error_from_grpc_status( + "DEADLINE_EXCEEDED", +@@ -770,16 +833,19 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Server, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Server, ++ .. ++ } ++ )); + } + + #[test] + fn error_display_messages() { + let err = Error::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail { + status_code: Some(401), + ..ProviderErrorDetail::new("invalid api key", "openai") +@@ -792,7 +858,7 @@ mod tests { + + let err = Error::Configuration { + message: "no provider".into(), +- source: None, ++ source: None, + }; + assert_eq!(err.to_string(), "Configuration error: no provider"); + } +@@ -800,7 +866,7 @@ mod tests { + #[test] + fn status_code_accessor() { + let err = Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail { + status_code: Some(503), + ..ProviderErrorDetail::new("error", "openai") +@@ -810,7 +876,7 @@ mod tests { + + let err = Error::Network { + message: "refused".into(), +- source: None, ++ source: None, + }; + assert_eq!(err.status_code(), None); + } +@@ -818,7 +884,7 @@ mod tests { + #[test] + fn provider_name_from_provider_variant() { + let err = Error::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), + }; + assert_eq!(err.provider_name(), "openai"); +@@ -828,7 +894,7 @@ mod tests { + fn provider_name_defaults_to_unknown() { + let err = Error::Network { + message: "refused".into(), +- source: None, ++ source: None, + }; + assert_eq!(err.provider_name(), "unknown"); + } +@@ -839,7 +905,7 @@ mod tests { + + assert!( + Error::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: detail(), + } + .failover_eligible() +@@ -847,7 +913,7 @@ mod tests { + + assert!( + Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: detail(), + } + .failover_eligible() +@@ -855,7 +921,7 @@ mod tests { + + assert!( + Error::Provider { +- kind: ProviderErrorKind::QuotaExceeded, ++ kind: ProviderErrorKind::QuotaExceeded, + detail: detail(), + } + .failover_eligible() +@@ -867,7 +933,7 @@ mod tests { + assert!( + Error::RequestTimeout { + message: "timed out".into(), +- source: None, ++ source: None, + } + .failover_eligible() + ); +@@ -875,7 +941,7 @@ mod tests { + assert!( + Error::Network { + message: "refused".into(), +- source: None, ++ source: None, + } + .failover_eligible() + ); +@@ -883,7 +949,7 @@ mod tests { + assert!( + Error::Stream { + message: "broken".into(), +- source: None, ++ source: None, + } + .failover_eligible() + ); +@@ -895,7 +961,7 @@ mod tests { + + assert!( + !Error::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: detail(), + } + .failover_eligible() +@@ -903,7 +969,7 @@ mod tests { + + assert!( + !Error::Provider { +- kind: ProviderErrorKind::InvalidRequest, ++ kind: ProviderErrorKind::InvalidRequest, + detail: detail(), + } + .failover_eligible() +@@ -911,7 +977,7 @@ mod tests { + + assert!( + !Error::Provider { +- kind: ProviderErrorKind::ContextLength, ++ kind: ProviderErrorKind::ContextLength, + detail: detail(), + } + .failover_eligible() +@@ -919,7 +985,7 @@ mod tests { + + assert!( + !Error::Provider { +- kind: ProviderErrorKind::ContentFilter, ++ kind: ProviderErrorKind::ContentFilter, + detail: detail(), + } + .failover_eligible() +@@ -931,7 +997,7 @@ mod tests { + assert!( + !Error::Configuration { + message: "bad".into(), +- source: None, ++ source: None, + } + .failover_eligible() + ); +@@ -968,7 +1034,7 @@ mod tests { + #[test] + fn failure_signature_hint_provider_transient() { + let err = Error::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), + }; + assert_eq!( +@@ -977,7 +1043,7 @@ mod tests { + ); + + let err = Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail::new("500", "anthropic")), + }; + assert_eq!( +@@ -989,7 +1055,7 @@ mod tests { + #[test] + fn failure_signature_hint_provider_deterministic() { + let err = Error::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), + }; + assert_eq!( +@@ -998,7 +1064,7 @@ mod tests { + ); + + let err = Error::Provider { +- kind: ProviderErrorKind::AccessDenied, ++ kind: ProviderErrorKind::AccessDenied, + detail: Box::new(ProviderErrorDetail::new("denied", "anthropic")), + }; + assert_eq!( +@@ -1007,7 +1073,7 @@ mod tests { + ); + + let err = Error::Provider { +- kind: ProviderErrorKind::NotFound, ++ kind: ProviderErrorKind::NotFound, + detail: Box::new(ProviderErrorDetail::new("missing", "openai")), + }; + assert_eq!( +@@ -1016,7 +1082,7 @@ mod tests { + ); + + let err = Error::Provider { +- kind: ProviderErrorKind::InvalidRequest, ++ kind: ProviderErrorKind::InvalidRequest, + detail: Box::new(ProviderErrorDetail::new("bad", "openai")), + }; + assert_eq!( +@@ -1025,7 +1091,7 @@ mod tests { + ); + + let err = Error::Provider { +- kind: ProviderErrorKind::ContentFilter, ++ kind: ProviderErrorKind::ContentFilter, + detail: Box::new(ProviderErrorDetail::new("blocked", "openai")), + }; + assert_eq!( +@@ -1034,7 +1100,7 @@ mod tests { + ); + + let err = Error::Provider { +- kind: ProviderErrorKind::ContextLength, ++ kind: ProviderErrorKind::ContextLength, + detail: Box::new(ProviderErrorDetail::new("too long", "openai")), + }; + assert_eq!( +@@ -1043,7 +1109,7 @@ mod tests { + ); + + let err = Error::Provider { +- kind: ProviderErrorKind::QuotaExceeded, ++ kind: ProviderErrorKind::QuotaExceeded, + detail: Box::new(ProviderErrorDetail::new("out of quota", "openai")), + }; + assert_eq!( +@@ -1057,7 +1123,7 @@ mod tests { + assert_eq!( + Error::RequestTimeout { + message: "timed out".into(), +- source: None, ++ source: None, + } + .failure_signature_hint(), + "api_transient|unknown|timeout" +@@ -1065,7 +1131,7 @@ mod tests { + assert_eq!( + Error::Network { + message: "refused".into(), +- source: None, ++ source: None, + } + .failure_signature_hint(), + "api_transient|unknown|network" +@@ -1073,7 +1139,7 @@ mod tests { + assert_eq!( + Error::Stream { + message: "broken".into(), +- source: None, ++ source: None, + } + .failure_signature_hint(), + "api_transient|unknown|stream" +@@ -1088,7 +1154,7 @@ mod tests { + assert_eq!( + Error::Configuration { + message: "bad".into(), +- source: None, ++ source: None, + } + .failure_signature_hint(), + "api_deterministic|unknown|configuration" +diff --git a/lib/crates/fabro-llm/src/generate.rs b/lib/crates/fabro-llm/src/generate.rs +index f73c42b1..16f6ce2d 100644 +--- a/lib/crates/fabro-llm/src/generate.rs ++++ b/lib/crates/fabro-llm/src/generate.rs +@@ -48,7 +48,7 @@ fn build_initial_messages(params: &GenerateParams) -> Result, Error + if params.messages.is_some() { + return Err(Error::Configuration { + message: "Cannot specify both 'prompt' and 'messages'".into(), +- source: None, ++ source: None, + }); + } + messages.push(Message::user(prompt)); +@@ -64,19 +64,19 @@ fn build_request( + tool_definitions: Option<&[ToolDefinition]>, + ) -> Request { + Request { +- model: params.model.clone(), +- messages: messages.to_vec(), +- provider: params.provider.clone(), +- tools: tool_definitions.map(<[ToolDefinition]>::to_vec), +- tool_choice: params.tool_choice.clone(), +- response_format: params.response_format.clone(), +- temperature: params.temperature, +- top_p: params.top_p, +- max_tokens: params.max_tokens, +- stop_sequences: params.stop_sequences.clone(), ++ model: params.model.clone(), ++ messages: messages.to_vec(), ++ provider: params.provider.clone(), ++ tools: tool_definitions.map(<[ToolDefinition]>::to_vec), ++ tool_choice: params.tool_choice.clone(), ++ response_format: params.response_format.clone(), ++ temperature: params.temperature, ++ top_p: params.top_p, ++ max_tokens: params.max_tokens, ++ stop_sequences: params.stop_sequences.clone(), + reasoning_effort: params.reasoning_effort, +- speed: params.speed.clone(), +- metadata: params.metadata.clone(), ++ speed: params.speed.clone(), ++ metadata: params.metadata.clone(), + provider_options: params.provider_options.clone(), + } + } +@@ -175,7 +175,7 @@ pub async fn generate(params: GenerateParams) -> Result { + warn!(timeout_secs = per_step, "Per-step timeout exceeded"); + Error::RequestTimeout { + message: format!("Per-step timeout of {per_step}s exceeded"), +- source: None, ++ source: None, + } + })? + } else { +@@ -274,7 +274,7 @@ pub async fn generate(params: GenerateParams) -> Result { + warn!(timeout_secs = total, "Total generation timeout exceeded"); + Error::RequestTimeout { + message: format!("Total timeout of {total}s exceeded"), +- source: None, ++ source: None, + } + })? + } else { +@@ -288,30 +288,30 @@ pub type StopCondition = Arc bool + Send + Sync>; + /// Parameters for `generate()` (Section 4.3). + #[derive(Clone)] + pub struct GenerateParams { +- pub model: String, +- pub prompt: Option, +- pub messages: Option>, +- pub system: Option, +- pub tools: Option>>, +- pub tool_choice: Option, +- pub max_tool_rounds: u32, +- pub response_format: Option, +- pub temperature: Option, +- pub top_p: Option, +- pub max_tokens: Option, +- pub stop_sequences: Option>, ++ pub model: String, ++ pub prompt: Option, ++ pub messages: Option>, ++ pub system: Option, ++ pub tools: Option>>, ++ pub tool_choice: Option, ++ pub max_tool_rounds: u32, ++ pub response_format: Option, ++ pub temperature: Option, ++ pub top_p: Option, ++ pub max_tokens: Option, ++ pub stop_sequences: Option>, + pub reasoning_effort: Option, +- pub speed: Option, +- pub provider: Option, ++ pub speed: Option, ++ pub provider: Option, + pub provider_options: Option, +- pub metadata: Option>, +- pub max_retries: u32, +- pub timeout: Option, +- pub client: Option>, ++ pub metadata: Option>, ++ pub max_retries: u32, ++ pub timeout: Option, ++ pub client: Option>, + /// Cancellation token to interrupt generation (Section 4.8). +- pub abort_signal: Option, ++ pub abort_signal: Option, + /// Custom stop condition checked after each tool round (Section 4.3). +- pub stop_when: Option, ++ pub stop_when: Option, + /// Callback to repair invalid tool call arguments (Section 5.8). + pub repair_tool_call: Option, + } +@@ -319,28 +319,28 @@ pub struct GenerateParams { + impl GenerateParams { + pub fn new(model: impl Into) -> Self { + Self { +- model: model.into(), +- prompt: None, +- messages: None, +- system: None, +- tools: None, +- tool_choice: None, +- max_tool_rounds: 1, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: None, +- stop_sequences: None, ++ model: model.into(), ++ prompt: None, ++ messages: None, ++ system: None, ++ tools: None, ++ tool_choice: None, ++ max_tool_rounds: 1, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: None, ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- provider: None, ++ speed: None, ++ provider: None, + provider_options: None, +- metadata: None, +- max_retries: 2, +- timeout: None, +- client: None, +- abort_signal: None, +- stop_when: None, ++ metadata: None, ++ max_retries: 2, ++ timeout: None, ++ client: None, ++ abort_signal: None, ++ stop_when: None, + repair_tool_call: None, + } + } +@@ -479,24 +479,24 @@ impl GenerateParams { + /// `StreamAccumulator` collects stream events into a complete Response (Section + /// 4.4). + pub struct StreamAccumulator { +- text_parts: Vec, ++ text_parts: Vec, + reasoning_parts: Vec, +- tool_calls: Vec, +- finish_reason: Option, +- usage: Option, +- response: Option, ++ tool_calls: Vec, ++ finish_reason: Option, ++ usage: Option, ++ response: Option, + } + + impl StreamAccumulator { + #[must_use] + pub const fn new() -> Self { + Self { +- text_parts: Vec::new(), ++ text_parts: Vec::new(), + reasoning_parts: Vec::new(), +- tool_calls: Vec::new(), +- finish_reason: None, +- usage: None, +- response: None, ++ tool_calls: Vec::new(), ++ finish_reason: None, ++ usage: None, ++ response: None, + } + } + +@@ -563,7 +563,7 @@ impl Default for StreamAccumulator { + /// as a drop-in replacement for `StreamEventStream`. Also supports multi-step + /// tool loops when active tools are provided. + pub struct StreamResult { +- inner: StreamEventStream, ++ inner: StreamEventStream, + accumulator: StreamAccumulator, + } + +@@ -716,7 +716,7 @@ async fn stream_with_tool_loop(params: GenerateParams) -> Result Result Result Some(( + Err(Error::RequestTimeout { + message: format!("Total timeout of {total_copy}s exceeded"), +- source: None, ++ source: None, + }), + (stream, true), + )), +@@ -956,9 +956,9 @@ pub async fn generate_object( + ) -> Result { + let params = GenerateParams { + response_format: Some(ResponseFormat { +- kind: ResponseFormatType::JsonSchema, ++ kind: ResponseFormatType::JsonSchema, + json_schema: Some(schema), +- strict: true, ++ strict: true, + }), + ..params + }; +@@ -988,7 +988,7 @@ pub type ObjectStream = + /// used as a drop-in replacement for `ObjectStream`. Tracks the last `Complete` + /// event's object internally so callers can retrieve it after the stream ends. + pub struct ObjectStreamResult { +- inner: ObjectStream, ++ inner: ObjectStream, + object: Option, + } + +@@ -1045,9 +1045,9 @@ pub async fn stream_object( + ) -> Result { + let params = GenerateParams { + response_format: Some(ResponseFormat { +- kind: ResponseFormatType::JsonSchema, ++ kind: ResponseFormatType::JsonSchema, + json_schema: Some(schema), +- strict: true, ++ strict: true, + }), + ..params + }; +@@ -1081,7 +1081,7 @@ pub async fn stream_object( + match serde_json::from_str::(accumulated_text) { + Ok(final_object) => { + events.push(Ok(ObjectStreamEvent::Complete { +- object: final_object, ++ object: final_object, + response: response.clone(), + })); + } +@@ -1101,7 +1101,7 @@ pub async fn stream_object( + Err(e) => { + events.push(Err(Error::Stream { + message: format!("{e}"), +- source: None, ++ source: None, + })); + } + } +@@ -1148,19 +1148,19 @@ mod tests { + + async fn complete(&self, _request: &Request) -> Result { + Ok(Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(&self.response_text), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(&self.response_text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 20, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -1176,19 +1176,19 @@ mod tests { + ..Default::default() + }, + Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(&text), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(&text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 20, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }, + )), + ]; +@@ -1281,45 +1281,45 @@ mod tests { + if count == 0 { + // First call: return tool call + Ok(Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(ToolCall::new( ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message { ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(ToolCall::new( + "call_1", + "get_weather", + serde_json::json!({"city": "SF"}), + ))], +- name: None, ++ name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 5, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } else { + // Second call: return text + Ok(Response { +- id: "resp_2".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant("The weather in SF is 72F"), ++ id: "resp_2".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant("The weather in SF is 72F"), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 20, + output_tokens: 10, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + } +@@ -1377,19 +1377,19 @@ mod tests { + acc.process(&StreamEvent::text_delta(" world", Some("t1".into()))); + + let resp = Response { +- id: "r1".into(), +- model: "m".into(), +- provider: "p".into(), +- message: Message::assistant("Hello world"), ++ id: "r1".into(), ++ model: "m".into(), ++ provider: "p".into(), ++ message: Message::assistant("Hello world"), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 5, + output_tokens: 2, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + + acc.process(&StreamEvent::finish( +@@ -1534,9 +1534,9 @@ mod tests { + .max_retries(5) + .tool_choice(ToolChoice::Required) + .response_format(ResponseFormat { +- kind: ResponseFormatType::JsonObject, ++ kind: ResponseFormatType::JsonObject, + json_schema: None, +- strict: false, ++ strict: false, + }) + .max_tool_rounds(3); + +@@ -1559,7 +1559,7 @@ mod tests { + #[test] + fn generate_params_timeout_builder() { + let params = GenerateParams::new("test-model").timeout(TimeoutOptions { +- total: Some(30.0), ++ total: Some(30.0), + per_step: Some(10.0), + }); + assert!(params.timeout.is_some()); +@@ -1570,7 +1570,7 @@ mod tests { + + /// Mock provider that streams JSON tokens incrementally. + struct StreamingJsonMockProvider { +- deltas: Vec, ++ deltas: Vec, + full_text: String, + } + +@@ -1592,15 +1592,15 @@ mod tests { + + async fn complete(&self, _request: &Request) -> Result { + Ok(Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(&self.full_text), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(&self.full_text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -1619,19 +1619,19 @@ mod tests { + ..Default::default() + }, + Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(&self.full_text), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(&self.full_text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 20, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }, + ))); + +@@ -1783,7 +1783,7 @@ mod tests { + async fn generate_abort_signal_between_tool_rounds() { + // Provider that always returns tool calls + struct AlwaysToolCallProvider { +- call_count: Arc, ++ call_count: Arc, + cancel_token: CancellationToken, + } + +@@ -1800,24 +1800,24 @@ mod tests { + self.cancel_token.cancel(); + } + Ok(Response { +- id: format!("resp_{count}"), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(ToolCall::new( ++ id: format!("resp_{count}"), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message { ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(ToolCall::new( + format!("call_{count}"), + "get_weather", + serde_json::json!({"city": "SF"}), + ))], +- name: None, ++ name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -1831,7 +1831,7 @@ mod tests { + let token_clone = token.clone(); + + let provider: Arc = Arc::new(AlwaysToolCallProvider { +- call_count: call_count.clone(), ++ call_count: call_count.clone(), + cancel_token: token_clone, + }); + let mut providers: HashMap> = HashMap::new(); +@@ -1986,15 +1986,15 @@ mod tests { + + async fn complete(&self, _request: &Request) -> Result { + Ok(Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant("fallback"), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant("fallback"), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -2006,24 +2006,24 @@ mod tests { + let tool_call = + ToolCall::new("call_1", "get_weather", serde_json::json!({"city": "SF"})); + let response = Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(tool_call.clone())], +- name: None, ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message { ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(tool_call.clone())], ++ name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 5, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + let events = vec![ + Ok(StreamEvent::ToolCallEnd { tool_call }), +@@ -2038,19 +2038,19 @@ mod tests { + // Second stream: return text + let text = "The weather in SF is 72F"; + let response = Response { +- id: "resp_2".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(text), ++ id: "resp_2".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 20, + output_tokens: 10, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + let events = vec![ + Ok(StreamEvent::text_delta(text, Some("t1".into()))), +@@ -2156,19 +2156,19 @@ mod tests { + let mut acc = StreamAccumulator::new(); + + let response = Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant("tool step"), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant("tool step"), + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 5, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + + let tool_calls = vec![ToolCall::new( +@@ -2314,7 +2314,7 @@ mod tests { + /// Mock provider that fails on stream N times then succeeds + struct FailThenStreamProvider { + call_count: Arc, +- failures: u32, ++ failures: u32, + } + + #[async_trait::async_trait] +@@ -2325,15 +2325,15 @@ mod tests { + + async fn complete(&self, _request: &Request) -> Result { + Ok(Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant("fallback"), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant("fallback"), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -2342,7 +2342,7 @@ mod tests { + + if count < self.failures { + return Err(Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail { + status_code: Some(500), + ..ProviderErrorDetail::new("server error", "mock") +@@ -2352,19 +2352,19 @@ mod tests { + + let text = "Hello after retry"; + let response = Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(text), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 20, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + let events = vec![ + Ok(StreamEvent::text_delta(text, Some("t1".into()))), +@@ -2383,7 +2383,7 @@ mod tests { + let call_count = Arc::new(AtomicU32::new(0)); + let provider: Arc = Arc::new(FailThenStreamProvider { + call_count: call_count.clone(), +- failures: 2, // fail twice, succeed on third ++ failures: 2, // fail twice, succeed on third + }); + + let mut providers: HashMap> = HashMap::new(); +@@ -2439,15 +2439,15 @@ mod tests { + + async fn complete(&self, _request: &Request) -> Result { + Ok(Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant("fallback"), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant("fallback"), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -2455,15 +2455,15 @@ mod tests { + sleep(self.delay).await; + let text = "Slow response"; + let response = Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(text), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + let events = vec![ + Ok(StreamEvent::text_delta(text, Some("t1".into()))), +@@ -2538,15 +2538,15 @@ mod tests { + + async fn complete(&self, _request: &Request) -> Result { + Ok(Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant("fallback"), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant("fallback"), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -2558,20 +2558,20 @@ mod tests { + let tool_call = + ToolCall::new("call_1", "get_weather", serde_json::json!({"city": "SF"})); + let response = Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(tool_call.clone())], +- name: None, ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message { ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(tool_call.clone())], ++ name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + let events = vec![ + Ok(StreamEvent::ToolCallEnd { tool_call }), +@@ -2587,15 +2587,15 @@ mod tests { + sleep(std::time::Duration::from_secs(5)).await; + let text = "Should not arrive"; + let response = Response { +- id: "resp_2".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(text), ++ id: "resp_2".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + let events = vec![ + Ok(StreamEvent::text_delta(text, Some("t1".into()))), +diff --git a/lib/crates/fabro-llm/src/model_test.rs b/lib/crates/fabro-llm/src/model_test.rs +index edf6e815..4fe1efce 100644 +--- a/lib/crates/fabro-llm/src/model_test.rs ++++ b/lib/crates/fabro-llm/src/model_test.rs +@@ -65,7 +65,7 @@ impl ModelTestStatus { + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ModelTestOutcome { +- pub status: ModelTestStatus, ++ pub status: ModelTestStatus, + pub error_message: Option, + } + +@@ -73,7 +73,7 @@ impl ModelTestOutcome { + #[must_use] + pub fn ok() -> Self { + Self { +- status: ModelTestStatus::Ok, ++ status: ModelTestStatus::Ok, + error_message: None, + } + } +@@ -81,7 +81,7 @@ impl ModelTestOutcome { + #[must_use] + pub fn error(message: impl Into) -> Self { + Self { +- status: ModelTestStatus::Error, ++ status: ModelTestStatus::Error, + error_message: Some(message.into()), + } + } +@@ -234,14 +234,14 @@ mod tests { + display_name: "Test Model".to_string(), + limits: ModelLimits { + context_window: 200_000, +- max_output: Some(8_000), ++ max_output: Some(8_000), + }, + training: None, + knowledge_cutoff: None, + features, + costs: ModelCosts { +- input_cost_per_mtok: None, +- output_cost_per_mtok: None, ++ input_cost_per_mtok: None, ++ output_cost_per_mtok: None, + cache_input_cost_per_mtok: None, + }, + estimated_output_tps: None, +@@ -252,25 +252,25 @@ mod tests { + + fn response_with_text(text: &str) -> Response { + Response { +- id: "resp_1".to_string(), +- model: "test-model".to_string(), +- provider: "anthropic".to_string(), +- message: Message::assistant(text), ++ id: "resp_1".to_string(), ++ model: "test-model".to_string(), ++ provider: "anthropic".to_string(), ++ message: Message::assistant(text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + } + } + + #[tokio::test] + async fn run_model_test_deep_errors_when_model_lacks_tools() { + let info = test_model_with(ModelFeatures { +- tools: false, +- vision: false, ++ tools: false, ++ vision: false, + reasoning: true, +- effort: true, ++ effort: true, + }); + + let outcome = run_model_test(&info, ModelTestMode::Deep).await; +@@ -286,11 +286,11 @@ mod tests { + fn validate_deep_result_does_not_fail_only_for_missing_reasoning() { + let tool_results = vec![ToolResult::success("call_1", serde_json::json!(42))]; + let first_step = StepResult { +- response: response_with_text("tool step"), ++ response: response_with_text("tool step"), + tool_results: tool_results.clone(), + }; + let second_step = StepResult { +- response: response_with_text("84 is even"), ++ response: response_with_text("84 is even"), + tool_results: vec![], + }; + let result = GenerateResult { +diff --git a/lib/crates/fabro-llm/src/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs +index bfc1b701..99ca3a88 100644 +--- a/lib/crates/fabro-llm/src/providers/anthropic.rs ++++ b/lib/crates/fabro-llm/src/providers/anthropic.rs +@@ -17,14 +17,14 @@ use crate::types::{ + /// Provider adapter for the Anthropic Messages API. + pub struct Adapter { + pub(crate) http: super::http_api::HttpApi, +- provider_name: String, ++ provider_name: String, + } + + impl Adapter { + #[must_use] + pub fn new(api_key: impl Into) -> Self { + Self { +- http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL), ++ http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL), + provider_name: "anthropic".to_string(), + } + } +@@ -78,7 +78,7 @@ impl Adapter { + + response.ok_or_else(|| Error::Stream { + message: "complete_via_stream: stream ended without a Finish event".to_string(), +- source: None, ++ source: None, + }) + } + } +@@ -89,51 +89,51 @@ const DEFAULT_BASE_URL: &str = "https://api.anthropic.com/v1"; + + #[derive(serde::Serialize)] + struct ApiRequest { +- model: String, +- messages: Vec, +- max_tokens: i64, ++ model: String, ++ messages: Vec, ++ max_tokens: i64, + /// System prompt: either a plain string or an array of content blocks + /// (with optional `cache_control` annotations for prompt caching). + #[serde(skip_serializing_if = "Option::is_none")] +- system: Option, ++ system: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- temperature: Option, ++ temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- top_p: Option, ++ top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stop_sequences: Option>, + #[serde(skip_serializing_if = "Option::is_none")] +- tools: Option>, ++ tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] +- tool_choice: Option, ++ tool_choice: Option, + /// Extended thinking configuration (e.g. `{"type": "enabled", + /// "budget_tokens": 10000}`). Passed through from + /// `provider_options.anthropic.thinking`. + #[serde(skip_serializing_if = "Option::is_none")] +- thinking: Option, ++ thinking: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- output_config: Option, ++ output_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- speed: Option, ++ speed: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- metadata: Option>, ++ metadata: Option>, + #[serde(skip_serializing_if = "std::ops::Not::not")] +- stream: bool, ++ stream: bool, + } + + /// Anthropic messages use structured content blocks, not plain strings. + #[derive(serde::Serialize)] + struct ApiMessage { +- role: String, ++ role: String, + content: Vec, + } + + /// Anthropic tool definition format. + #[derive(serde::Serialize)] + struct ApiToolDef { +- name: String, +- description: String, +- input_schema: serde_json::Value, ++ name: String, ++ description: String, ++ input_schema: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + cache_control: Option, + } +@@ -157,20 +157,20 @@ impl CacheControl { + + #[derive(serde::Deserialize)] + struct ApiResponse { +- id: String, +- model: String, +- content: Vec, ++ id: String, ++ model: String, ++ content: Vec, + stop_reason: Option, +- usage: ApiUsage, ++ usage: ApiUsage, + } + + #[derive(serde::Deserialize)] + #[allow(clippy::struct_field_names)] // Match Anthropic's API usage payload field names. + struct ApiUsage { +- input_tokens: i64, +- output_tokens: i64, ++ input_tokens: i64, ++ output_tokens: i64, + #[serde(default)] +- cache_read_input_tokens: Option, ++ cache_read_input_tokens: Option, + #[serde(default)] + cache_creation_input_tokens: Option, + } +@@ -211,21 +211,21 @@ fn parse_content_block(block: &serde_json::Value) -> Option { + block.get("input")?.clone(), + ))), + "thinking" => Some(ContentPart::Thinking(ThinkingData { +- text: block.get("thinking")?.as_str()?.to_string(), ++ text: block.get("thinking")?.as_str()?.to_string(), + signature: block + .get("signature") + .and_then(serde_json::Value::as_str) + .map(String::from), +- redacted: false, ++ redacted: false, + })), + "redacted_thinking" => Some(ContentPart::Thinking(ThinkingData { +- text: block ++ text: block + .get("data") + .and_then(serde_json::Value::as_str) + .unwrap_or("") + .to_string(), + signature: None, +- redacted: true, ++ redacted: true, + })), + _ => None, + } +@@ -360,9 +360,9 @@ fn translate_tools(tools: &[ToolDefinition]) -> Vec { + tools + .iter() + .map(|t| ApiToolDef { +- name: t.name.clone(), +- description: t.description.clone(), +- input_schema: t.parameters.clone(), ++ name: t.name.clone(), ++ description: t.description.clone(), ++ input_schema: t.parameters.clone(), + cache_control: None, + }) + .collect() +@@ -409,9 +409,9 @@ fn apply_response_format( + .clone() + .unwrap_or_else(|| serde_json::json!({"type": "object"})); + let synthetic_tool = ApiToolDef { +- name: SYNTHETIC_TOOL_NAME.to_string(), +- description: "Output the requested structured data".to_string(), +- input_schema: schema, ++ name: SYNTHETIC_TOOL_NAME.to_string(), ++ description: "Output the requested structured data".to_string(), ++ input_schema: schema, + cache_control: None, + }; + match api_tools { +@@ -667,21 +667,21 @@ enum ContentBlockKind { + + /// Accumulated state across SSE events during streaming. + struct StreamAccumulator { +- id: String, +- model: String, +- content_parts: Vec, +- usage: TokenCounts, +- finish_reason: FinishReason, ++ id: String, ++ model: String, ++ content_parts: Vec, ++ usage: TokenCounts, ++ finish_reason: FinishReason, + /// The kind of the current content block, set by `content_block_start`. +- current_block: Option, ++ current_block: Option, + /// Accumulated text for the current text block. +- current_text: String, ++ current_text: String, + /// Accumulated thinking text for the current thinking block. +- current_thinking: String, ++ current_thinking: String, + /// Accumulated raw JSON arguments for the current `tool_use` block. + current_tool_args: String, + /// Rate limit info parsed from the initial HTTP response headers. +- rate_limit: Option, ++ rate_limit: Option, + } + + impl StreamAccumulator { +@@ -705,20 +705,20 @@ impl StreamAccumulator { + fn take_response(&mut self) -> Response { + let content_parts = std::mem::take(&mut self.content_parts); + Response { +- id: self.id.clone(), +- model: self.model.clone(), +- provider: "anthropic".to_string(), +- message: Message { +- role: Role::Assistant, +- content: content_parts, +- name: None, ++ id: self.id.clone(), ++ model: self.model.clone(), ++ provider: "anthropic".to_string(), ++ message: Message { ++ role: Role::Assistant, ++ content: content_parts, ++ name: None, + tool_call_id: None, + }, + finish_reason: self.finish_reason.clone(), +- usage: self.usage.clone(), +- raw: None, +- warnings: vec![], +- rate_limit: self.rate_limit.clone(), ++ usage: self.usage.clone(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: self.rate_limit.clone(), + } + } + } +@@ -782,7 +782,7 @@ impl StreamAccumulator { + .unwrap_or("") + .to_string(); + self.current_block = Some(ContentBlockKind::ToolUse { +- id: id.clone(), ++ id: id.clone(), + name: name.clone(), + }); + self.current_tool_args.clear(); +@@ -825,7 +825,7 @@ impl StreamAccumulator { + .unwrap_or(0); + + vec![StreamEvent::TextDelta { +- delta: text.to_string(), ++ delta: text.to_string(), + text_id: Some(format!("block_{index}")), + }] + } +@@ -911,9 +911,9 @@ impl StreamAccumulator { + .and_then(serde_json::Value::as_str) + .map(String::from); + self.content_parts.push(ContentPart::Thinking(ThinkingData { +- text: thinking_text, ++ text: thinking_text, + signature: stop_signature.or(signature), +- redacted: false, ++ redacted: false, + })); + vec![StreamEvent::ReasoningEnd] + } +@@ -941,8 +941,8 @@ impl StreamAccumulator { + let response = self.take_response(); + vec![StreamEvent::Finish { + finish_reason: response.finish_reason.clone(), +- usage: response.usage.clone(), +- response: Box::new(response), ++ usage: response.usage.clone(), ++ response: Box::new(response), + }] + } + } +@@ -970,18 +970,15 @@ fn process_sse_event( + // --- SSE reader --- + + enum SseResult { +- Event { +- event_type: String, +- data: String, +- }, ++ Event { event_type: String, data: String }, + Done, + Error(Error), + } + + struct SseReaderState { +- line_reader: super::common::LineReader, +- accumulator: StreamAccumulator, +- pending_events: std::collections::VecDeque, ++ line_reader: super::common::LineReader, ++ accumulator: StreamAccumulator, ++ pending_events: std::collections::VecDeque, + /// When true, `tool_use` events for the synthetic tool are converted to + /// text events. + json_schema_mode: bool, +@@ -1285,9 +1282,9 @@ impl ProviderAdapter for Adapter { + model: api_resp.model, + provider: self.provider_name.clone(), + message: Message { +- role: Role::Assistant, +- content: content_parts, +- name: None, ++ role: Role::Assistant, ++ content: content_parts, ++ name: None, + tool_call_id: None, + }, + finish_reason, +@@ -1449,15 +1446,15 @@ mod tests { + fn tool_cache_control_applied_to_last_tool() { + let mut tools = vec![ + ApiToolDef { +- name: "tool_a".to_string(), +- description: "first".to_string(), +- input_schema: serde_json::json!({}), ++ name: "tool_a".to_string(), ++ description: "first".to_string(), ++ input_schema: serde_json::json!({}), + cache_control: None, + }, + ApiToolDef { +- name: "tool_b".to_string(), +- description: "second".to_string(), +- input_schema: serde_json::json!({}), ++ name: "tool_b".to_string(), ++ description: "second".to_string(), ++ input_schema: serde_json::json!({}), + cache_control: None, + }, + ]; +@@ -1478,9 +1475,9 @@ mod tests { + #[test] + fn tool_cache_control_single_tool() { + let mut tools = vec![ApiToolDef { +- name: "only_tool".to_string(), +- description: "the one".to_string(), +- input_schema: serde_json::json!({}), ++ name: "only_tool".to_string(), ++ description: "the one".to_string(), ++ input_schema: serde_json::json!({}), + cache_control: None, + }]; + apply_cache_control_to_last_tool(&mut tools); +@@ -1491,15 +1488,15 @@ mod tests { + fn conversation_prefix_cache_control_with_two_user_messages() { + let mut messages = vec![ + ApiMessage { +- role: "user".to_string(), ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Hello"})], + }, + ApiMessage { +- role: "assistant".to_string(), ++ role: "assistant".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Hi there"})], + }, + ApiMessage { +- role: "user".to_string(), ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "How are you?"})], + }, + ]; +@@ -1518,18 +1515,18 @@ mod tests { + fn conversation_prefix_cache_control_with_multiple_content_blocks() { + let mut messages = vec![ + ApiMessage { +- role: "user".to_string(), ++ role: "user".to_string(), + content: vec![ + serde_json::json!({"type": "text", "text": "Part 1"}), + serde_json::json!({"type": "text", "text": "Part 2"}), + ], + }, + ApiMessage { +- role: "assistant".to_string(), ++ role: "assistant".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Reply"})], + }, + ApiMessage { +- role: "user".to_string(), ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Follow up"})], + }, + ]; +@@ -1545,7 +1542,7 @@ mod tests { + #[test] + fn conversation_prefix_cache_control_single_user_message() { + let mut messages = vec![ApiMessage { +- role: "user".to_string(), ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Hello"})], + }]; + +@@ -1566,23 +1563,23 @@ mod tests { + fn conversation_prefix_cache_control_three_user_messages() { + let mut messages = vec![ + ApiMessage { +- role: "user".to_string(), ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "First"})], + }, + ApiMessage { +- role: "assistant".to_string(), ++ role: "assistant".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Reply 1"})], + }, + ApiMessage { +- role: "user".to_string(), ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Second"})], + }, + ApiMessage { +- role: "assistant".to_string(), ++ role: "assistant".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Reply 2"})], + }, + ApiMessage { +- role: "user".to_string(), ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Third"})], + }, + ]; +@@ -1649,9 +1646,9 @@ mod tests { + #[test] + fn tool_serialization_includes_cache_control() { + let tool = ApiToolDef { +- name: "test_tool".to_string(), +- description: "A test tool".to_string(), +- input_schema: serde_json::json!({"type": "object"}), ++ name: "test_tool".to_string(), ++ description: "A test tool".to_string(), ++ input_schema: serde_json::json!({"type": "object"}), + cache_control: Some(CacheControl::ephemeral()), + }; + let json = serde_json::to_value(&tool).expect("should serialize"); +@@ -1661,9 +1658,9 @@ mod tests { + #[test] + fn tool_serialization_omits_cache_control_when_none() { + let tool = ApiToolDef { +- name: "test_tool".to_string(), +- description: "A test tool".to_string(), +- input_schema: serde_json::json!({"type": "object"}), ++ name: "test_tool".to_string(), ++ description: "A test tool".to_string(), ++ input_schema: serde_json::json!({"type": "object"}), + cache_control: None, + }; + let json = serde_json::to_value(&tool).expect("should serialize"); +@@ -1680,23 +1677,23 @@ mod tests { + #[test] + fn api_request_serialization_with_cached_system() { + let api_request = ApiRequest { +- model: "claude-sonnet-4-20250514".to_string(), +- messages: vec![ApiMessage { +- role: "user".to_string(), ++ model: "claude-sonnet-4-20250514".to_string(), ++ messages: vec![ApiMessage { ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Hello"})], + }], +- max_tokens: 4096, +- system: Some(system_with_cache_control("You are helpful.")), +- temperature: None, +- top_p: None, ++ max_tokens: 4096, ++ system: Some(system_with_cache_control("You are helpful.")), ++ temperature: None, ++ top_p: None, + stop_sequences: None, +- tools: None, +- tool_choice: None, +- thinking: None, +- output_config: None, +- speed: None, +- metadata: None, +- stream: false, ++ tools: None, ++ tool_choice: None, ++ thinking: None, ++ output_config: None, ++ speed: None, ++ metadata: None, ++ stream: false, + }; + + let json = serde_json::to_value(&api_request).expect("should serialize"); +@@ -1723,19 +1720,19 @@ mod tests { + + fn make_base_request() -> Request { + Request { +- model: "claude-sonnet-4-20250514".to_string(), +- messages: vec![Message::user("Hello")], +- provider: Some("anthropic".to_string()), +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: Some(128), +- stop_sequences: None, ++ model: "claude-sonnet-4-20250514".to_string(), ++ messages: vec![Message::user("Hello")], ++ provider: Some("anthropic".to_string()), ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: Some(128), ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + } + } +@@ -1757,9 +1754,9 @@ mod tests { + "required": ["name"] + }); + let request = make_request_with_format(ResponseFormat { +- kind: ResponseFormatType::JsonSchema, ++ kind: ResponseFormatType::JsonSchema, + json_schema: Some(schema.clone()), +- strict: false, ++ strict: false, + }); + + let mut tools: Option> = None; +@@ -1785,14 +1782,14 @@ mod tests { + fn response_format_json_schema_appends_to_existing_tools() { + let schema = serde_json::json!({"type": "object"}); + let mut request = make_request_with_format(ResponseFormat { +- kind: ResponseFormatType::JsonSchema, ++ kind: ResponseFormatType::JsonSchema, + json_schema: Some(schema), +- strict: false, ++ strict: false, + }); + request.tools = Some(vec![ToolDefinition { +- name: "existing_tool".to_string(), ++ name: "existing_tool".to_string(), + description: "An existing tool".to_string(), +- parameters: serde_json::json!({}), ++ parameters: serde_json::json!({}), + }]); + + let mut tools: Option> = +@@ -1811,9 +1808,9 @@ mod tests { + #[test] + fn response_format_json_object_appends_to_string_system() { + let request = make_request_with_format(ResponseFormat { +- kind: ResponseFormatType::JsonObject, ++ kind: ResponseFormatType::JsonObject, + json_schema: None, +- strict: false, ++ strict: false, + }); + + let mut tools: Option> = None; +@@ -1835,9 +1832,9 @@ mod tests { + #[test] + fn response_format_json_object_sets_system_when_none() { + let request = make_request_with_format(ResponseFormat { +- kind: ResponseFormatType::JsonObject, ++ kind: ResponseFormatType::JsonObject, + json_schema: None, +- strict: false, ++ strict: false, + }); + + let mut tools: Option> = None; +@@ -1854,9 +1851,9 @@ mod tests { + #[test] + fn response_format_json_object_appends_to_array_system() { + let request = make_request_with_format(ResponseFormat { +- kind: ResponseFormatType::JsonObject, ++ kind: ResponseFormatType::JsonObject, + json_schema: None, +- strict: false, ++ strict: false, + }); + + let mut tools: Option> = None; +@@ -1875,9 +1872,9 @@ mod tests { + #[test] + fn response_format_text_is_noop() { + let request = make_request_with_format(ResponseFormat { +- kind: ResponseFormatType::Text, ++ kind: ResponseFormatType::Text, + json_schema: None, +- strict: false, ++ strict: false, + }); + + let mut tools: Option> = None; +@@ -1960,24 +1957,24 @@ mod tests { + #[test] + fn convert_stream_event_converts_finish_reason() { + let response = Box::new(Response { +- id: "test".to_string(), +- model: "claude".to_string(), +- provider: "anthropic".to_string(), +- message: Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(ToolCall::new( ++ id: "test".to_string(), ++ model: "claude".to_string(), ++ provider: "anthropic".to_string(), ++ message: Message { ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(ToolCall::new( + "id1", + SYNTHETIC_TOOL_NAME, + serde_json::json!({"data": "value"}), + ))], +- name: None, ++ name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }); + let event = StreamEvent::Finish { + finish_reason: FinishReason::ToolCalls, +@@ -2003,10 +2000,10 @@ mod tests { + #[test] + fn document_url_translates_to_url_source() { + let part = ContentPart::Document(DocumentData { +- url: Some("https://example.com/doc.pdf".to_string()), +- data: None, ++ url: Some("https://example.com/doc.pdf".to_string()), ++ data: None, + media_type: None, +- file_name: None, ++ file_name: None, + }); + let result = content_part_to_api(&part).expect("should produce JSON"); + assert_eq!(result["type"], "document"); +@@ -2017,10 +2014,10 @@ mod tests { + #[test] + fn document_base64_data_translates_to_base64_source() { + let part = ContentPart::Document(DocumentData { +- url: None, +- data: Some(vec![0x25, 0x50, 0x44, 0x46]), ++ url: None, ++ data: Some(vec![0x25, 0x50, 0x44, 0x46]), + media_type: Some("application/pdf".to_string()), +- file_name: Some("test.pdf".to_string()), ++ file_name: Some("test.pdf".to_string()), + }); + let result = content_part_to_api(&part).expect("should produce JSON"); + assert_eq!(result["type"], "document"); +@@ -2032,10 +2029,10 @@ mod tests { + #[test] + fn document_base64_defaults_to_pdf_mime() { + let part = ContentPart::Document(DocumentData { +- url: None, +- data: Some(vec![1, 2, 3]), ++ url: None, ++ data: Some(vec![1, 2, 3]), + media_type: None, +- file_name: None, ++ file_name: None, + }); + let result = content_part_to_api(&part).expect("should produce JSON"); + assert_eq!(result["source"]["media_type"], "application/pdf"); +@@ -2077,23 +2074,23 @@ mod tests { + #[test] + fn merge_provider_options_passes_through_unknown_keys() { + let api_request = ApiRequest { +- model: "claude-sonnet-4-20250514".to_string(), +- messages: vec![ApiMessage { +- role: "user".to_string(), ++ model: "claude-sonnet-4-20250514".to_string(), ++ messages: vec![ApiMessage { ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Hello"})], + }], +- max_tokens: 4096, +- system: None, +- temperature: None, +- top_p: None, ++ max_tokens: 4096, ++ system: None, ++ temperature: None, ++ top_p: None, + stop_sequences: None, +- tools: None, +- tool_choice: None, +- thinking: None, +- output_config: None, +- speed: None, +- metadata: None, +- stream: false, ++ tools: None, ++ tool_choice: None, ++ thinking: None, ++ output_config: None, ++ speed: None, ++ metadata: None, ++ stream: false, + }; + + let opts = serde_json::json!({ +@@ -2110,23 +2107,23 @@ mod tests { + #[test] + fn merge_provider_options_skips_known_keys() { + let api_request = ApiRequest { +- model: "claude-sonnet-4-20250514".to_string(), +- messages: vec![ApiMessage { +- role: "user".to_string(), ++ model: "claude-sonnet-4-20250514".to_string(), ++ messages: vec![ApiMessage { ++ role: "user".to_string(), + content: vec![serde_json::json!({"type": "text", "text": "Hello"})], + }], +- max_tokens: 4096, +- system: None, +- temperature: None, +- top_p: None, ++ max_tokens: 4096, ++ system: None, ++ temperature: None, ++ top_p: None, + stop_sequences: None, +- tools: None, +- tool_choice: None, +- thinking: None, +- output_config: None, +- speed: None, +- metadata: None, +- stream: false, ++ tools: None, ++ tool_choice: None, ++ thinking: None, ++ output_config: None, ++ speed: None, ++ metadata: None, ++ stream: false, + }; + + let opts = serde_json::json!({ +@@ -2151,8 +2148,8 @@ mod tests { + #[test] + fn audio_produces_text_fallback() { + let part = ContentPart::Audio(AudioData { +- url: Some("https://example.com/audio.wav".to_string()), +- data: None, ++ url: Some("https://example.com/audio.wav".to_string()), ++ data: None, + media_type: None, + }); + let result = content_part_to_api(&part).expect("should produce JSON"); +diff --git a/lib/crates/fabro-llm/src/providers/common.rs b/lib/crates/fabro-llm/src/providers/common.rs +index 2a227476..d606519e 100644 +--- a/lib/crates/fabro-llm/src/providers/common.rs ++++ b/lib/crates/fabro-llm/src/providers/common.rs +@@ -213,8 +213,8 @@ pub async fn send_and_read_response( + /// configurable delimiter (e.g. `"\n"` for Gemini/OpenAI-compatible, `"\n\n"` + /// for Anthropic/OpenAI SSE event blocks). + pub struct LineReader { +- response: fabro_http::Response, +- buffer: String, ++ response: fabro_http::Response, ++ buffer: String, + stream_read_timeout: Option, + } + +@@ -267,7 +267,7 @@ impl LineReader { + warn!("Stream read timed out waiting for next event"); + return Err(Error::Stream { + message: "stream read timed out waiting for next event".to_string(), +- source: None, ++ source: None, + }); + } + } +@@ -467,9 +467,9 @@ mod tests { + #[test] + fn extract_system_prompt_developer_role() { + let dev = Message { +- role: Role::Developer, +- content: vec![ContentPart::text("dev instructions")], +- name: None, ++ role: Role::Developer, ++ content: vec![ContentPart::text("dev instructions")], ++ name: None, + tool_call_id: None, + }; + let msgs = vec![dev, Message::user("hi")]; +@@ -481,9 +481,9 @@ mod tests { + #[test] + fn extract_system_prompt_ignores_whitespace_system_and_developer() { + let dev = Message { +- role: Role::Developer, +- content: vec![ContentPart::text(" \n\t ")], +- name: None, ++ role: Role::Developer, ++ content: vec![ContentPart::text(" \n\t ")], ++ name: None, + tool_call_id: None, + }; + let msgs = vec![Message::system(" "), dev, Message::user("hi")]; +diff --git a/lib/crates/fabro-llm/src/providers/fabro_server.rs b/lib/crates/fabro-llm/src/providers/fabro_server.rs +index da9c643e..3a686234 100644 +--- a/lib/crates/fabro-llm/src/providers/fabro_server.rs ++++ b/lib/crates/fabro-llm/src/providers/fabro_server.rs +@@ -10,8 +10,8 @@ use crate::types::{FinishReason, Message, Request, Response, StreamEvent, TokenC + /// `/completions` endpoint, delegating to whatever real provider the server + /// is configured with. + pub struct Adapter { +- client: fabro_http::HttpClient, +- base_url: String, ++ client: fabro_http::HttpClient, ++ base_url: String, + provider_name: String, + } + +@@ -35,16 +35,16 @@ impl Adapter { + + #[derive(serde::Deserialize)] + struct ServerCompletionResponse { +- id: String, +- model: String, +- message: Message, ++ id: String, ++ model: String, ++ message: Message, + stop_reason: String, +- usage: ServerUsage, ++ usage: ServerUsage, + } + + #[derive(serde::Deserialize)] + struct ServerUsage { +- input_tokens: i64, ++ input_tokens: i64, + output_tokens: i64, + } + +@@ -230,19 +230,19 @@ mod tests { + + fn make_request() -> Request { + Request { +- model: "test-model".to_string(), +- messages: vec![Message::user("Hello")], +- provider: None, +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: None, +- stop_sequences: None, ++ model: "test-model".to_string(), ++ messages: vec![Message::user("Hello")], ++ provider: None, ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: None, ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + } + } +diff --git a/lib/crates/fabro-llm/src/providers/gemini.rs b/lib/crates/fabro-llm/src/providers/gemini.rs +index e66d81f2..6273a1ff 100644 +--- a/lib/crates/fabro-llm/src/providers/gemini.rs ++++ b/lib/crates/fabro-llm/src/providers/gemini.rs +@@ -58,20 +58,20 @@ impl Adapter { + #[derive(serde::Serialize)] + #[serde(rename_all = "camelCase")] + struct ApiRequest { +- contents: Vec, ++ contents: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + system_instruction: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- generation_config: Option, ++ generation_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- tools: Option>, ++ tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] +- tool_config: Option, ++ tool_config: Option, + } + + #[derive(serde::Serialize)] + struct Content { +- role: String, ++ role: String, + parts: Vec, + } + +@@ -84,17 +84,17 @@ struct SystemInstruction { + #[serde(rename_all = "camelCase")] + struct GenerationOptions { + #[serde(skip_serializing_if = "Option::is_none")] +- temperature: Option, ++ temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- max_output_tokens: Option, ++ max_output_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- top_p: Option, ++ top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- stop_sequences: Option>, ++ stop_sequences: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + response_mime_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- response_schema: Option, ++ response_schema: Option, + } + + /// Gemini groups function declarations under a `tools` array. +@@ -106,9 +106,9 @@ struct GeminiToolGroup { + + #[derive(serde::Serialize)] + struct GeminiFunctionDecl { +- name: String, ++ name: String, + description: String, +- parameters: serde_json::Value, ++ parameters: serde_json::Value, + } + + // --- Response types --- +@@ -116,14 +116,14 @@ struct GeminiFunctionDecl { + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct ApiResponse { +- candidates: Option>, ++ candidates: Option>, + usage_metadata: Option, + } + + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct Candidate { +- content: Option, ++ content: Option, + finish_reason: Option, + } + +@@ -136,9 +136,9 @@ struct CandidateContent { + #[serde(rename_all = "camelCase")] + #[allow(clippy::struct_field_names)] + struct UsageMetadata { +- prompt_token_count: Option, +- candidates_token_count: Option, +- thoughts_token_count: Option, ++ prompt_token_count: Option, ++ candidates_token_count: Option, ++ thoughts_token_count: Option, + cached_content_token_count: Option, + } + +@@ -163,9 +163,9 @@ fn parse_part(part: &serde_json::Value) -> Option { + .unwrap_or(false); + if is_thought { + return Some(ContentPart::Thinking(ThinkingData { +- text: text.to_string(), ++ text: text.to_string(), + signature: None, +- redacted: false, ++ redacted: false, + })); + } + return Some(ContentPart::text(text)); +@@ -359,9 +359,9 @@ fn translate_tools(tools: &[ToolDefinition]) -> Vec { + function_declarations: tools + .iter() + .map(|t| GeminiFunctionDecl { +- name: t.name.clone(), ++ name: t.name.clone(), + description: t.description.clone(), +- parameters: t.parameters.clone(), ++ parameters: t.parameters.clone(), + }) + .collect(), + }] +@@ -682,32 +682,32 @@ fn process_sse_stream( + + /// Internal state for the SSE stream processor. + struct SseStreamState { +- line_reader: super::common::LineReader, +- model: String, ++ line_reader: super::common::LineReader, ++ model: String, + /// Events extracted from a chunk but not yet yielded. +- pending_events: std::collections::VecDeque, ++ pending_events: std::collections::VecDeque, + /// Whether we have emitted a `StreamStart` event. +- stream_started: bool, ++ stream_started: bool, + /// Whether we have emitted a `TextStart` event. +- text_started: bool, ++ text_started: bool, + /// Whether we are currently inside a reasoning (thought) segment. +- reasoning_started: bool, ++ reasoning_started: bool, + /// Accumulated thinking text across all chunks. +- accumulated_thinking: String, ++ accumulated_thinking: String, + /// Accumulated text across all chunks. +- accumulated_text: String, ++ accumulated_text: String, + /// Accumulated tool calls across all chunks. + accumulated_tool_calls: Vec, + /// The `text_id` used for `TextStart`/`TextDelta`/`TextEnd`. +- text_id: String, ++ text_id: String, + /// Latest usage metadata (updated per chunk; final chunk has totals). +- usage: TokenCounts, ++ usage: TokenCounts, + /// The finish reason string from the candidate, if received. +- finish_reason_str: Option, ++ finish_reason_str: Option, + /// Whether we have emitted the `Finish` event. +- finished: bool, ++ finished: bool, + /// Rate limit info parsed from HTTP response headers. +- rate_limit: Option, ++ rate_limit: Option, + } + + impl SseStreamState { +@@ -852,9 +852,9 @@ impl SseStreamState { + let mut content_parts: Vec = Vec::new(); + if !self.accumulated_thinking.is_empty() { + content_parts.push(ContentPart::Thinking(ThinkingData { +- text: self.accumulated_thinking.clone(), ++ text: self.accumulated_thinking.clone(), + signature: None, +- redacted: false, ++ redacted: false, + })); + } + if !self.accumulated_text.is_empty() { +@@ -865,20 +865,20 @@ impl SseStreamState { + } + + let response = Response { +- id: uuid::Uuid::new_v4().to_string(), +- model: self.model.clone(), +- provider: "gemini".to_string(), +- message: Message { +- role: Role::Assistant, +- content: content_parts, +- name: None, ++ id: uuid::Uuid::new_v4().to_string(), ++ model: self.model.clone(), ++ provider: "gemini".to_string(), ++ message: Message { ++ role: Role::Assistant, ++ content: content_parts, ++ name: None, + tool_call_id: None, + }, + finish_reason: finish_reason.clone(), +- usage: self.usage.clone(), +- raw: None, +- warnings: vec![], +- rate_limit: self.rate_limit.clone(), ++ usage: self.usage.clone(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: self.rate_limit.clone(), + }; + + StreamEvent::finish(finish_reason, self.usage.clone(), response) +@@ -920,7 +920,7 @@ impl ProviderAdapter for Adapter { + .as_ref() + .and_then(|c| c.first()) + .ok_or_else(|| Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail::new( + "no candidates in Gemini response", + "gemini", +@@ -944,9 +944,9 @@ impl ProviderAdapter for Adapter { + model: request.model.clone(), + provider: "gemini".to_string(), + message: Message { +- role: Role::Assistant, +- content: content_parts, +- name: None, ++ role: Role::Assistant, ++ content: content_parts, ++ name: None, + tool_call_id: None, + }, + finish_reason, +@@ -991,19 +991,19 @@ mod tests { + + fn minimal_request() -> Request { + Request { +- model: "gemini-2.0-flash".to_string(), +- messages: vec![Message::user("Hello")], +- provider: None, +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: None, +- stop_sequences: None, ++ model: "gemini-2.0-flash".to_string(), ++ messages: vec![Message::user("Hello")], ++ provider: None, ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: None, ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + } + } +@@ -1136,13 +1136,13 @@ mod tests { + #[test] + fn audio_url_translates_to_file_data() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Audio(AudioData { +- url: Some("https://example.com/audio.wav".to_string()), +- data: None, ++ role: Role::User, ++ content: vec![ContentPart::Audio(AudioData { ++ url: Some("https://example.com/audio.wav".to_string()), ++ data: None, + media_type: Some("audio/wav".to_string()), + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let contents = translate_messages(&[&msg]); +@@ -1155,13 +1155,13 @@ mod tests { + #[test] + fn audio_base64_translates_to_inline_data() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Audio(AudioData { +- url: None, +- data: Some(vec![0xFF, 0xFB, 0x90]), ++ role: Role::User, ++ content: vec![ContentPart::Audio(AudioData { ++ url: None, ++ data: Some(vec![0xFF, 0xFB, 0x90]), + media_type: None, + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let contents = translate_messages(&[&msg]); +@@ -1173,14 +1173,14 @@ mod tests { + #[test] + fn document_url_translates_to_file_data() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Document(DocumentData { +- url: Some("https://example.com/doc.pdf".to_string()), +- data: None, ++ role: Role::User, ++ content: vec![ContentPart::Document(DocumentData { ++ url: Some("https://example.com/doc.pdf".to_string()), ++ data: None, + media_type: Some("application/pdf".to_string()), +- file_name: Some("doc.pdf".to_string()), ++ file_name: Some("doc.pdf".to_string()), + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let contents = translate_messages(&[&msg]); +@@ -1192,14 +1192,14 @@ mod tests { + #[test] + fn document_base64_translates_to_inline_data() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Document(DocumentData { +- url: None, +- data: Some(vec![0x25, 0x50, 0x44, 0x46]), ++ role: Role::User, ++ content: vec![ContentPart::Document(DocumentData { ++ url: None, ++ data: Some(vec![0x25, 0x50, 0x44, 0x46]), + media_type: None, +- file_name: None, ++ file_name: None, + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let contents = translate_messages(&[&msg]); +@@ -1219,10 +1219,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::NotFound, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::NotFound, ++ .. ++ } ++ )); + + let err = gemini_error( + 400, +@@ -1231,10 +1234,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::InvalidRequest, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::InvalidRequest, ++ .. ++ } ++ )); + + let err = gemini_error( + 429, +@@ -1243,10 +1249,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::RateLimit, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::RateLimit, ++ .. ++ } ++ )); + + let err = gemini_error( + 401, +@@ -1255,10 +1264,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Authentication, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Authentication, ++ .. ++ } ++ )); + + let err = gemini_error( + 403, +@@ -1267,10 +1279,13 @@ mod tests { + None, + None, + ); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::AccessDenied, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::AccessDenied, ++ .. ++ } ++ )); + + let err = gemini_error( + 504, +@@ -1287,16 +1302,22 @@ mod tests { + use crate::error::ProviderErrorKind; + + let err = gemini_error(429, "rate limited".into(), None, None, None); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::RateLimit, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::RateLimit, ++ .. ++ } ++ )); + + let err = gemini_error(500, "internal".into(), None, None, None); +- assert!(matches!(err, Error::Provider { +- kind: ProviderErrorKind::Server, +- .. +- })); ++ assert!(matches!( ++ err, ++ Error::Provider { ++ kind: ProviderErrorKind::Server, ++ .. ++ } ++ )); + } + + #[test] +@@ -1374,9 +1395,9 @@ mod tests { + tc.provider_metadata = Some(serde_json::json!({"thoughtSignature": "sig456"})); + + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(tc)], +- name: None, ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(tc)], ++ name: None, + tool_call_id: None, + }; + let contents = translate_messages(&[&msg]); +@@ -1396,9 +1417,9 @@ mod tests { + ); + + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(tc)], +- name: None, ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(tc)], ++ name: None, + tool_call_id: None, + }; + let contents = translate_messages(&[&msg]); +diff --git a/lib/crates/fabro-llm/src/providers/http_api.rs b/lib/crates/fabro-llm/src/providers/http_api.rs +index 09e29e44..31622509 100644 +--- a/lib/crates/fabro-llm/src/providers/http_api.rs ++++ b/lib/crates/fabro-llm/src/providers/http_api.rs +@@ -9,11 +9,11 @@ use crate::types::AdapterTimeout; + /// configuration that every provider needs. Provider-specific fields live on + /// the adapter struct itself. + pub struct HttpApi { +- pub(crate) api_key: String, +- pub(crate) base_url: String, +- pub(crate) default_headers: HashMap, +- pub(crate) client: fabro_http::HttpClient, +- pub(crate) request_timeout: Option, ++ pub(crate) api_key: String, ++ pub(crate) base_url: String, ++ pub(crate) default_headers: HashMap, ++ pub(crate) client: fabro_http::HttpClient, ++ pub(crate) request_timeout: Option, + pub(crate) stream_read_timeout: Option, + } + +diff --git a/lib/crates/fabro-llm/src/providers/openai.rs b/lib/crates/fabro-llm/src/providers/openai.rs +index 23d5d020..a0bb977c 100644 +--- a/lib/crates/fabro-llm/src/providers/openai.rs ++++ b/lib/crates/fabro-llm/src/providers/openai.rs +@@ -23,18 +23,18 @@ const DEFAULT_BASE_URL: &str = "https://api.openai.com/v1"; + /// server-side state. + pub struct Adapter { + pub(crate) http: super::http_api::HttpApi, +- org_id: Option, +- project_id: Option, ++ org_id: Option, ++ project_id: Option, + /// When true, always use streaming (required by the Codex endpoint). +- codex_mode: bool, ++ codex_mode: bool, + } + + impl Adapter { + #[must_use] + pub fn new(api_key: impl Into) -> Self { + Self { +- http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL), +- org_id: None, ++ http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL), ++ org_id: None, + project_id: None, + codex_mode: false, + } +@@ -112,7 +112,7 @@ impl Adapter { + } + last_response.ok_or_else(|| Error::Network { + message: "Stream ended without a finish event".into(), +- source: None, ++ source: None, + }) + } + } +@@ -121,52 +121,52 @@ impl Adapter { + + #[derive(serde::Serialize)] + struct ApiRequest { +- model: String, +- input: Vec, ++ model: String, ++ input: Vec, + #[serde(skip_serializing_if = "Option::is_none")] +- instructions: Option, ++ instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- temperature: Option, ++ temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_output_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- top_p: Option, ++ top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- tools: Option>, ++ tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] +- tool_choice: Option, ++ tool_choice: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- reasoning: Option, ++ reasoning: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- text: Option, ++ text: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- stop: Option>, ++ stop: Option>, + #[serde(skip_serializing_if = "Option::is_none")] +- metadata: Option>, +- store: bool, ++ metadata: Option>, ++ store: bool, + #[serde(skip_serializing_if = "Vec::is_empty")] +- include: Vec, ++ include: Vec, + #[serde(skip_serializing_if = "std::ops::Not::not")] +- stream: bool, ++ stream: bool, + } + + // --- Response types (Responses API format) --- + + #[derive(serde::Deserialize)] + struct ApiResponse { +- id: String, +- model: Option, ++ id: String, ++ model: Option, + output: Vec, + status: Option, +- usage: Option, ++ usage: Option, + } + + #[derive(serde::Deserialize)] + struct ApiUsage { +- input_tokens: i64, +- output_tokens: i64, ++ input_tokens: i64, ++ output_tokens: i64, + output_tokens_details: Option, +- input_tokens_details: Option, ++ input_tokens_details: Option, + } + + #[derive(serde::Deserialize)] +@@ -537,23 +537,23 @@ fn parse_output(output: &[serde_json::Value]) -> (Vec, bool) { + + /// Mutable state carried through SSE stream processing. + struct SseStreamState { +- line_reader: super::common::LineReader, +- model: String, +- response_id: String, +- response_model: String, +- accumulated_text: String, +- tool_calls: Vec, ++ line_reader: super::common::LineReader, ++ model: String, ++ response_id: String, ++ response_model: String, ++ accumulated_text: String, ++ tool_calls: Vec, + /// Raw reasoning output items to preserve for round-tripping. +- reasoning_items: Vec, ++ reasoning_items: Vec, + /// Raw message output items to preserve for round-tripping. +- message_items: Vec, +- usage: TokenCounts, +- finish_reason: FinishReason, +- emitted_start: bool, +- emitted_text_start: bool, ++ message_items: Vec, ++ usage: TokenCounts, ++ finish_reason: FinishReason, ++ emitted_start: bool, ++ emitted_text_start: bool, + emitted_reasoning_start: bool, +- raw_response: Option, +- rate_limit: Option, ++ raw_response: Option, ++ rate_limit: Option, + } + + /// Parse a single SSE message block into an (`event_type`, `data`) pair. +@@ -918,9 +918,9 @@ fn handle_response_completed( + model, + provider: "openai".to_string(), + message: Message { +- role: Role::Assistant, +- content: content_parts, +- name: None, ++ role: Role::Assistant, ++ content: content_parts, ++ name: None, + tool_call_id: None, + }, + finish_reason: state.finish_reason.clone(), +@@ -994,9 +994,9 @@ impl ProviderAdapter for Adapter { + model: api_resp.model.unwrap_or_else(|| request.model.clone()), + provider: "openai".to_string(), + message: Message { +- role: Role::Assistant, +- content: content_parts, +- name: None, ++ role: Role::Assistant, ++ content: content_parts, ++ name: None, + tool_call_id: None, + }, + finish_reason, +@@ -1086,19 +1086,19 @@ mod tests { + + fn minimal_request() -> Request { + Request { +- model: "gpt-4o".to_string(), +- messages: vec![Message::user("Hello")], +- provider: None, +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: None, +- stop_sequences: None, ++ model: "gpt-4o".to_string(), ++ messages: vec![Message::user("Hello")], ++ provider: None, ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: None, ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + } + } +@@ -1240,13 +1240,13 @@ mod tests { + #[test] + fn audio_content_produces_text_fallback() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Audio(AudioData { +- url: Some("https://example.com/audio.wav".to_string()), +- data: None, ++ role: Role::User, ++ content: vec![ContentPart::Audio(AudioData { ++ url: Some("https://example.com/audio.wav".to_string()), ++ data: None, + media_type: None, + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1263,14 +1263,14 @@ mod tests { + #[test] + fn document_content_produces_text_fallback_with_filename() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Document(DocumentData { +- url: Some("https://example.com/doc.pdf".to_string()), +- data: None, ++ role: Role::User, ++ content: vec![ContentPart::Document(DocumentData { ++ url: Some("https://example.com/doc.pdf".to_string()), ++ data: None, + media_type: None, +- file_name: Some("report.pdf".to_string()), ++ file_name: Some("report.pdf".to_string()), + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1287,14 +1287,14 @@ mod tests { + #[test] + fn document_content_produces_text_fallback_without_filename() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Document(DocumentData { +- url: None, +- data: Some(vec![1, 2, 3]), ++ role: Role::User, ++ content: vec![ContentPart::Document(DocumentData { ++ url: None, ++ data: Some(vec![1, 2, 3]), + media_type: None, +- file_name: None, ++ file_name: None, + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1345,9 +1345,9 @@ mod tests { + tc.provider_metadata = Some(serde_json::json!({"id": "fc_abc123"})); + + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(tc)], +- name: None, ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(tc)], ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1364,9 +1364,9 @@ mod tests { + let tc = ToolCall::new("call_xyz789", "get_weather", serde_json::json!({})); + + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(tc)], +- name: None, ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(tc)], ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1456,15 +1456,15 @@ mod tests { + tc.provider_metadata = Some(serde_json::json!({"id": "fc_def456"})); + + let msg = Message { +- role: Role::Assistant, +- content: vec![ ++ role: Role::Assistant, ++ content: vec![ + ContentPart::Other { + kind: ContentPart::OPENAI_REASONING.to_string(), + data: reasoning, + }, + ContentPart::ToolCall(tc), + ], +- name: None, ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1500,8 +1500,8 @@ mod tests { + tc.provider_metadata = Some(serde_json::json!({"id": "fc_def456"})); + + let msg = Message { +- role: Role::Assistant, +- content: vec![ ++ role: Role::Assistant, ++ content: vec![ + ContentPart::Other { + kind: ContentPart::OPENAI_REASONING.to_string(), + data: reasoning, +@@ -1513,7 +1513,7 @@ mod tests { + ContentPart::text("Checking now."), + ContentPart::ToolCall(tc), + ], +- name: None, ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1535,9 +1535,9 @@ mod tests { + // For non-OpenAI turns or turns without preserved message items, + // Text parts should still produce a constructed message. + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::text("Hello")], +- name: None, ++ role: Role::Assistant, ++ content: vec![ContentPart::text("Hello")], ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1562,9 +1562,9 @@ mod tests { + + // Now translate back to input format + let msg = Message { +- role: Role::Assistant, +- content: parts, +- name: None, ++ role: Role::Assistant, ++ content: parts, ++ name: None, + tool_call_id: None, + }; + let (_, input) = translate_input(&[msg]); +@@ -1599,21 +1599,21 @@ mod tests { + let http_resp = http::Response::builder().status(200).body("").unwrap(); + let response = fabro_http::Response::from(http_resp); + SseStreamState { +- line_reader: LineReader::new(response, None), +- model: String::new(), +- response_id: String::new(), +- response_model: String::new(), +- accumulated_text: String::new(), +- tool_calls: Vec::new(), +- reasoning_items: Vec::new(), +- message_items: Vec::new(), +- usage: TokenCounts::default(), +- finish_reason: FinishReason::Stop, +- emitted_start: true, +- emitted_text_start: false, ++ line_reader: LineReader::new(response, None), ++ model: String::new(), ++ response_id: String::new(), ++ response_model: String::new(), ++ accumulated_text: String::new(), ++ tool_calls: Vec::new(), ++ reasoning_items: Vec::new(), ++ message_items: Vec::new(), ++ usage: TokenCounts::default(), ++ finish_reason: FinishReason::Stop, ++ emitted_start: true, ++ emitted_text_start: false, + emitted_reasoning_start: false, +- raw_response: None, +- rate_limit: None, ++ raw_response: None, ++ rate_limit: None, + } + } + +diff --git a/lib/crates/fabro-llm/src/providers/openai_compatible.rs b/lib/crates/fabro-llm/src/providers/openai_compatible.rs +index 354b83b3..15c7d096 100644 +--- a/lib/crates/fabro-llm/src/providers/openai_compatible.rs ++++ b/lib/crates/fabro-llm/src/providers/openai_compatible.rs +@@ -20,14 +20,14 @@ use crate::types::{ + /// features. Use the primary `OpenAiAdapter` for `OpenAI`'s own API. + pub struct Adapter { + pub(crate) http: super::http_api::HttpApi, +- provider_name: String, ++ provider_name: String, + } + + impl Adapter { + #[must_use] + pub fn new(api_key: impl Into, base_url: impl Into) -> Self { + Self { +- http: super::http_api::HttpApi::new(api_key, base_url), ++ http: super::http_api::HttpApi::new(api_key, base_url), + provider_name: "openai-compatible".to_string(), + } + } +@@ -69,52 +69,52 @@ impl Adapter { + + #[derive(serde::Serialize)] + struct ApiRequest { +- model: String, +- messages: Vec, ++ model: String, ++ messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] +- temperature: Option, ++ temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- max_tokens: Option, ++ max_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- top_p: Option, ++ top_p: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- stop: Option>, ++ stop: Option>, + #[serde(skip_serializing_if = "Option::is_none")] +- tools: Option>, ++ tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] +- tool_choice: Option, ++ tool_choice: Option, + #[serde(skip_serializing_if = "Option::is_none")] + response_format: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- stream: Option, ++ stream: Option, + } + + #[derive(serde::Serialize)] + struct ChatMessage { +- role: String, ++ role: String, + #[serde(skip_serializing_if = "Option::is_none")] +- content: Option, ++ content: Option, + /// Reasoning/thinking content echoed back for providers that require it + /// (Kimi). + #[serde(skip_serializing_if = "Option::is_none")] + reasoning_content: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- tool_call_id: Option, ++ tool_call_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- tool_calls: Option>, ++ tool_calls: Option>, + } + + #[derive(serde::Serialize)] + struct ChatToolCall { +- id: String, ++ id: String, + #[serde(rename = "type")] +- kind: String, ++ kind: String, + function: ChatFunction, + } + + #[derive(serde::Serialize)] + struct ChatFunction { +- name: String, ++ name: String, + arguments: String, + } + +@@ -122,41 +122,41 @@ struct ChatFunction { + + #[derive(serde::Deserialize)] + struct ApiResponse { +- id: String, +- model: String, ++ id: String, ++ model: String, + choices: Vec, +- usage: Option, ++ usage: Option, + } + + #[derive(serde::Deserialize)] + struct ApiChoice { +- message: ApiChoiceMessage, ++ message: ApiChoiceMessage, + finish_reason: Option, + } + + #[derive(serde::Deserialize)] + struct ApiChoiceMessage { +- content: Option, ++ content: Option, + reasoning_content: Option, +- tool_calls: Option>, ++ tool_calls: Option>, + } + + #[derive(serde::Deserialize)] + struct ApiToolCall { +- id: String, ++ id: String, + function: ApiFunction, + } + + #[derive(serde::Deserialize)] + struct ApiFunction { +- name: String, ++ name: String, + arguments: String, + } + + #[derive(serde::Deserialize)] + #[allow(clippy::struct_field_names)] + struct ApiUsage { +- prompt_tokens: i64, ++ prompt_tokens: i64, + completion_tokens: i64, + } + +@@ -164,46 +164,46 @@ struct ApiUsage { + + #[derive(serde::Deserialize)] + struct StreamChunk { +- id: Option, +- model: Option, ++ id: Option, ++ model: Option, + choices: Option>, +- usage: Option, ++ usage: Option, + } + + #[derive(serde::Deserialize)] + struct StreamChoice { +- delta: Option, ++ delta: Option, + finish_reason: Option, + } + + #[derive(serde::Deserialize)] + struct StreamDelta { +- content: Option, ++ content: Option, + /// Reasoning/thinking content (used by Kimi and other reasoning models). + reasoning_content: Option, +- tool_calls: Option>, ++ tool_calls: Option>, + } + + #[derive(serde::Deserialize)] + struct StreamToolCall { +- index: usize, +- id: Option, ++ index: usize, ++ id: Option, + function: Option, + } + + #[derive(serde::Deserialize)] + struct StreamFunction { +- name: Option, ++ name: Option, + arguments: Option, + } + + // --- Accumulated tool call state for streaming --- + + struct AccumulatedToolCall { +- id: String, +- name: String, ++ id: String, ++ name: String, + arguments: String, +- started: bool, ++ started: bool, + } + + fn map_finish_reason(reason: Option<&str>) -> FinishReason { +@@ -259,11 +259,11 @@ fn translate_messages(messages: &[Message]) -> Vec { + .as_str() + .map_or_else(|| tr.content.to_string(), str::to_string); + Some(ChatMessage { +- role: "tool".to_string(), +- content: Some(output), ++ role: "tool".to_string(), ++ content: Some(output), + reasoning_content: None, +- tool_call_id: Some(tr.tool_call_id.clone()), +- tool_calls: None, ++ tool_call_id: Some(tr.tool_call_id.clone()), ++ tool_calls: None, + }) + } else { + None +@@ -288,8 +288,8 @@ fn translate_messages(messages: &[Message]) -> Vec { + .clone() + .unwrap_or_else(|| tc.arguments.to_string()); + tool_calls.push(ChatToolCall { +- id: tc.id.clone(), +- kind: "function".to_string(), ++ id: tc.id.clone(), ++ kind: "function".to_string(), + function: ChatFunction { + name: tc.name.clone(), + arguments, +@@ -470,7 +470,7 @@ impl ProviderAdapter for Adapter { + .map_err(|e| Error::network(format!("failed to parse response: {e}"), e))?; + + let choice = api_resp.choices.first().ok_or_else(|| Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail::new( + "no choices in response", + &self.provider_name, +@@ -481,9 +481,9 @@ impl ProviderAdapter for Adapter { + if let Some(reasoning) = &choice.message.reasoning_content { + if !reasoning.is_empty() { + content_parts.push(ContentPart::Thinking(ThinkingData { +- text: reasoning.clone(), ++ text: reasoning.clone(), + signature: None, +- redacted: false, ++ redacted: false, + })); + } + } +@@ -518,9 +518,9 @@ impl ProviderAdapter for Adapter { + model: api_resp.model, + provider: self.provider_name.clone(), + message: Message { +- role: Role::Assistant, +- content: content_parts, +- name: None, ++ role: Role::Assistant, ++ content: content_parts, ++ name: None, + tool_call_id: None, + }, + finish_reason, +@@ -634,7 +634,7 @@ impl ProviderAdapter for Adapter { + // Flatten batched events into individual stream events. + let flat_stream = stream::unfold( + FlattenState { +- inner: Box::pin(stream), ++ inner: Box::pin(stream), + pending: Vec::new(), + }, + |mut flatten_state| async { +@@ -662,28 +662,28 @@ impl ProviderAdapter for Adapter { + + /// State for flattening batched events into individual stream events. + struct FlattenState { +- inner: std::pin::Pin, Error>> + Send>>, ++ inner: std::pin::Pin, Error>> + Send>>, + pending: Vec, + } + + /// Accumulated state while processing the SSE stream. + struct StreamState { +- line_reader: super::common::LineReader, +- provider_name: String, +- model: String, +- response_id: String, +- response_model: String, +- accumulated_text: String, ++ line_reader: super::common::LineReader, ++ provider_name: String, ++ model: String, ++ response_id: String, ++ response_model: String, ++ accumulated_text: String, + accumulated_reasoning: String, +- tool_calls: Vec, +- usage: TokenCounts, +- finish_reason: FinishReason, +- text_started: bool, +- done: bool, ++ tool_calls: Vec, ++ usage: TokenCounts, ++ finish_reason: FinishReason, ++ text_started: bool, ++ done: bool, + /// True after `finish_events()` has been called (guards against + /// duplicates). +- finished: bool, +- rate_limit: Option, ++ finished: bool, ++ rate_limit: Option, + } + + impl StreamState { +@@ -787,10 +787,10 @@ impl StreamState { + // Grow the accumulated tool calls vector if needed. + while self.tool_calls.len() <= index { + self.tool_calls.push(AccumulatedToolCall { +- id: String::new(), +- name: String::new(), ++ id: String::new(), ++ name: String::new(), + arguments: String::new(), +- started: false, ++ started: false, + }); + } + +@@ -848,9 +848,9 @@ impl StreamState { + // Include reasoning/thinking content if present (Kimi, etc.). + if !self.accumulated_reasoning.is_empty() { + content_parts.push(ContentPart::Thinking(ThinkingData { +- text: std::mem::take(&mut self.accumulated_reasoning), ++ text: std::mem::take(&mut self.accumulated_reasoning), + signature: None, +- redacted: false, ++ redacted: false, + })); + } + +@@ -882,20 +882,20 @@ impl StreamState { + }; + + let response = Response { +- id: self.response_id.clone(), +- model: response_model, +- provider: self.provider_name.clone(), +- message: Message { +- role: Role::Assistant, +- content: content_parts, +- name: None, ++ id: self.response_id.clone(), ++ model: response_model, ++ provider: self.provider_name.clone(), ++ message: Message { ++ role: Role::Assistant, ++ content: content_parts, ++ name: None, + tool_call_id: None, + }, + finish_reason: self.finish_reason.clone(), +- usage: self.usage.clone(), +- raw: None, +- warnings: vec![], +- rate_limit: self.rate_limit.clone(), ++ usage: self.usage.clone(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: self.rate_limit.clone(), + }; + + events.push(StreamEvent::finish( +@@ -1085,10 +1085,10 @@ mod tests { + ); + state.response_id = "resp-1".into(); + state.tool_calls.push(AccumulatedToolCall { +- id: "call_1".into(), +- name: "get_weather".into(), ++ id: "call_1".into(), ++ name: "get_weather".into(), + arguments: r#"{"city":"SF"}"#.into(), +- started: true, ++ started: true, + }); + + let events = state.finish_events(); +@@ -1141,32 +1141,32 @@ mod tests { + #[test] + fn api_request_stream_field_serialization() { + let req = ApiRequest { +- model: "test".into(), +- messages: vec![], +- temperature: None, +- max_tokens: None, +- top_p: None, +- stop: None, +- tools: None, +- tool_choice: None, ++ model: "test".into(), ++ messages: vec![], ++ temperature: None, ++ max_tokens: None, ++ top_p: None, ++ stop: None, ++ tools: None, ++ tool_choice: None, + response_format: None, +- stream: Some(true), ++ stream: Some(true), + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["stream"], true); + + // When stream is None, it should be omitted. + let req_no_stream = ApiRequest { +- model: "test".into(), +- messages: vec![], +- temperature: None, +- max_tokens: None, +- top_p: None, +- stop: None, +- tools: None, +- tool_choice: None, ++ model: "test".into(), ++ messages: vec![], ++ temperature: None, ++ max_tokens: None, ++ top_p: None, ++ stop: None, ++ tools: None, ++ tool_choice: None, + response_format: None, +- stream: None, ++ stream: None, + }; + let json_no_stream = serde_json::to_value(&req_no_stream).unwrap(); + assert!(json_no_stream.get("stream").is_none()); +@@ -1175,13 +1175,13 @@ mod tests { + #[test] + fn translate_assistant_message_with_tool_calls_only() { + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(ToolCall::new( ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(ToolCall::new( + "call_1", + "get_weather", + serde_json::json!({"city": "SF"}), + ))], +- name: None, ++ name: None, + tool_call_id: None, + }; + let translated = translate_messages(&[msg]); +@@ -1199,8 +1199,8 @@ mod tests { + #[test] + fn translate_assistant_message_with_text_and_tool_calls() { + let msg = Message { +- role: Role::Assistant, +- content: vec![ ++ role: Role::Assistant, ++ content: vec![ + ContentPart::text("Let me check the weather"), + ContentPart::ToolCall(ToolCall::new( + "call_2", +@@ -1208,7 +1208,7 @@ mod tests { + serde_json::json!({"city": "NYC"}), + )), + ], +- name: None, ++ name: None, + tool_call_id: None, + }; + let translated = translate_messages(&[msg]); +@@ -1226,9 +1226,9 @@ mod tests { + let mut tc = ToolCall::new("call_3", "search", serde_json::json!({"q": "rust"})); + tc.raw_arguments = Some(r#"{"q": "rust"}"#.to_string()); + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(tc)], +- name: None, ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(tc)], ++ name: None, + tool_call_id: None, + }; + let translated = translate_messages(&[msg]); +@@ -1262,13 +1262,13 @@ mod tests { + #[test] + fn assistant_tool_calls_serialize_correctly() { + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(ToolCall::new( ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(ToolCall::new( + "call_1", + "get_weather", + serde_json::json!({"city": "SF"}), + ))], +- name: None, ++ name: None, + tool_call_id: None, + }; + let translated = translate_messages(&[msg]); +@@ -1284,19 +1284,19 @@ mod tests { + + fn minimal_request() -> Request { + Request { +- model: "llama-3.1-70b".to_string(), +- messages: vec![Message::user("Hello")], +- provider: None, +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: None, +- stop_sequences: None, ++ model: "llama-3.1-70b".to_string(), ++ messages: vec![Message::user("Hello")], ++ provider: None, ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: None, ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + } + } +@@ -1393,13 +1393,13 @@ mod tests { + #[test] + fn audio_content_produces_text_fallback() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Audio(AudioData { +- url: Some("https://example.com/audio.wav".to_string()), +- data: None, ++ role: Role::User, ++ content: vec![ContentPart::Audio(AudioData { ++ url: Some("https://example.com/audio.wav".to_string()), ++ data: None, + media_type: None, + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let translated = translate_messages(&[msg]); +@@ -1412,14 +1412,14 @@ mod tests { + #[test] + fn document_content_produces_text_fallback_with_filename() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Document(DocumentData { +- url: Some("https://example.com/doc.pdf".to_string()), +- data: None, ++ role: Role::User, ++ content: vec![ContentPart::Document(DocumentData { ++ url: Some("https://example.com/doc.pdf".to_string()), ++ data: None, + media_type: None, +- file_name: Some("report.pdf".to_string()), ++ file_name: Some("report.pdf".to_string()), + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let translated = translate_messages(&[msg]); +@@ -1432,14 +1432,14 @@ mod tests { + #[test] + fn document_content_produces_text_fallback_without_filename() { + let msg = Message { +- role: Role::User, +- content: vec![ContentPart::Document(DocumentData { +- url: None, +- data: Some(vec![1, 2, 3]), ++ role: Role::User, ++ content: vec![ContentPart::Document(DocumentData { ++ url: None, ++ data: Some(vec![1, 2, 3]), + media_type: None, +- file_name: None, ++ file_name: None, + })], +- name: None, ++ name: None, + tool_call_id: None, + }; + let translated = translate_messages(&[msg]); +@@ -1452,16 +1452,16 @@ mod tests { + #[test] + fn mixed_text_and_audio_content_concatenates() { + let msg = Message { +- role: Role::User, +- content: vec![ ++ role: Role::User, ++ content: vec![ + ContentPart::text("Check this: "), + ContentPart::Audio(AudioData { +- url: None, +- data: Some(vec![1, 2]), ++ url: None, ++ data: Some(vec![1, 2]), + media_type: None, + }), + ], +- name: None, ++ name: None, + tool_call_id: None, + }; + let translated = translate_messages(&[msg]); +diff --git a/lib/crates/fabro-llm/src/retry.rs b/lib/crates/fabro-llm/src/retry.rs +index 0feddc07..09ea45c8 100644 +--- a/lib/crates/fabro-llm/src/retry.rs ++++ b/lib/crates/fabro-llm/src/retry.rs +@@ -79,9 +79,9 @@ mod tests { + fn fast_backoff() -> BackoffPolicy { + BackoffPolicy { + initial_delay: Duration::from_micros(1), +- factor: 2.0, +- max_delay: Duration::from_secs(60), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(60), ++ jitter: false, + } + } + +@@ -129,7 +129,7 @@ mod tests { + let count = cc.fetch_add(1, Ordering::SeqCst); + if count < 2 { + Err(Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail { + status_code: Some(500), + ..ProviderErrorDetail::new("error", "test") +@@ -162,7 +162,7 @@ mod tests { + async move { + cc.fetch_add(1, Ordering::SeqCst); + Err::(Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail { + status_code: Some(500), + ..ProviderErrorDetail::new("error", "test") +@@ -192,7 +192,7 @@ mod tests { + async move { + cc.fetch_add(1, Ordering::SeqCst); + Err::(Error::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail { + status_code: Some(401), + ..ProviderErrorDetail::new("bad key", "test") +@@ -212,9 +212,9 @@ mod tests { + max_retries: 3, + backoff: BackoffPolicy { + initial_delay: Duration::from_micros(1), +- factor: 2.0, +- max_delay: Duration::from_secs(5), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(5), ++ jitter: false, + }, + ..Default::default() + }; +@@ -227,7 +227,7 @@ mod tests { + async move { + cc.fetch_add(1, Ordering::SeqCst); + Err::(Error::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail { + status_code: Some(429), + retry_after: Some(100.0), // Way beyond max_delay +@@ -248,9 +248,9 @@ mod tests { + max_retries: 1, + backoff: BackoffPolicy { + initial_delay: Duration::from_secs(10), // high, but retry_after is low +- factor: 2.0, +- max_delay: Duration::from_secs(60), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(60), ++ jitter: false, + }, + ..Default::default() + }; +@@ -265,7 +265,7 @@ mod tests { + let count = cc.fetch_add(1, Ordering::SeqCst); + if count < 1 { + Err(Error::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail { + status_code: Some(429), + retry_after: Some(0.01), +@@ -293,8 +293,8 @@ mod tests { + + let policy = RetryPolicy { + max_retries: 2, +- backoff: fast_backoff(), +- on_retry: Some(Arc::new(move |_err, _attempt, _delay| { ++ backoff: fast_backoff(), ++ on_retry: Some(Arc::new(move |_err, _attempt, _delay| { + retry_attempts_clone.fetch_add(1, Ordering::SeqCst); + })), + }; +@@ -308,7 +308,7 @@ mod tests { + let count = cc.fetch_add(1, Ordering::SeqCst); + if count < 2 { + Err(Error::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail { + status_code: Some(500), + ..ProviderErrorDetail::new("error", "test") +diff --git a/lib/crates/fabro-llm/src/tools.rs b/lib/crates/fabro-llm/src/tools.rs +index fc7b04c5..e15267af 100644 +--- a/lib/crates/fabro-llm/src/tools.rs ++++ b/lib/crates/fabro-llm/src/tools.rs +@@ -11,7 +11,7 @@ use crate::types::{Message, ToolCall, ToolDefinition, ToolResult}; + #[derive(Clone)] + pub struct ToolContext { + pub tool_call_id: String, +- pub messages: Vec, ++ pub messages: Vec, + pub abort_signal: Option, + } + +@@ -30,7 +30,7 @@ pub type ExecuteHandler = Arc< + /// "Passive" tools have no handler and are returned to the caller. + pub struct Tool { + pub definition: ToolDefinition, +- pub execute: Option, ++ pub execute: Option, + } + + impl Tool { +@@ -50,7 +50,7 @@ impl Tool { + description: description.to_string(), + parameters, + }, +- execute: None, ++ execute: None, + } + } + +@@ -78,7 +78,7 @@ impl Tool { + description: description.to_string(), + parameters, + }, +- execute: Some(Arc::new(move |args, ctx| Box::pin(handler(args, ctx)))), ++ execute: Some(Arc::new(move |args, ctx| Box::pin(handler(args, ctx)))), + } + } + +diff --git a/lib/crates/fabro-llm/src/types.rs b/lib/crates/fabro-llm/src/types.rs +index 89255b90..08597833 100644 +--- a/lib/crates/fabro-llm/src/types.rs ++++ b/lib/crates/fabro-llm/src/types.rs +@@ -22,32 +22,32 @@ pub enum Role { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct ImageData { +- pub url: Option, +- pub data: Option>, ++ pub url: Option, ++ pub data: Option>, + pub media_type: Option, +- pub detail: Option, ++ pub detail: Option, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct AudioData { +- pub url: Option, +- pub data: Option>, ++ pub url: Option, ++ pub data: Option>, + pub media_type: Option, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct DocumentData { +- pub url: Option, +- pub data: Option>, ++ pub url: Option, ++ pub data: Option>, + pub media_type: Option, +- pub file_name: Option, ++ pub file_name: Option, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct ThinkingData { +- pub text: String, ++ pub text: String, + pub signature: Option, +- pub redacted: bool, ++ pub redacted: bool, + } + + // --- 5.4 ToolCall / ToolResult --- +@@ -58,12 +58,12 @@ fn default_tool_type() -> String { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct ToolCall { +- pub id: String, +- pub name: String, ++ pub id: String, ++ pub name: String, + #[serde(rename = "type", default = "default_tool_type")] +- pub tool_type: String, +- pub arguments: serde_json::Value, +- pub raw_arguments: Option, ++ pub tool_type: String, ++ pub arguments: serde_json::Value, ++ pub raw_arguments: Option, + /// Opaque provider-specific metadata (e.g. Gemini `thought_signature`). + /// Preserved across round-trips so the provider can include it when + /// sending conversation history back to the API. +@@ -90,11 +90,11 @@ impl ToolCall { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct ToolResult { +- pub tool_call_id: String, +- pub content: serde_json::Value, +- pub is_error: bool, ++ pub tool_call_id: String, ++ pub content: serde_json::Value, ++ pub is_error: bool, + #[serde(skip_serializing_if = "Option::is_none")] +- pub image_data: Option>, ++ pub image_data: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub image_media_type: Option, + } +@@ -112,10 +112,10 @@ impl ToolResult { + + pub fn error(id: impl Into, message: impl Into) -> Self { + Self { +- tool_call_id: id.into(), +- content: serde_json::Value::String(message.into()), +- is_error: true, +- image_data: None, ++ tool_call_id: id.into(), ++ content: serde_json::Value::String(message.into()), ++ is_error: true, ++ image_data: None, + image_media_type: None, + } + } +@@ -253,36 +253,36 @@ impl ContentPart { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct Message { +- pub role: Role, +- pub content: Vec, +- pub name: Option, ++ pub role: Role, ++ pub content: Vec, ++ pub name: Option, + pub tool_call_id: Option, + } + + impl Message { + pub fn system(text: impl Into) -> Self { + Self { +- role: Role::System, +- content: vec![ContentPart::text(text)], +- name: None, ++ role: Role::System, ++ content: vec![ContentPart::text(text)], ++ name: None, + tool_call_id: None, + } + } + + pub fn user(text: impl Into) -> Self { + Self { +- role: Role::User, +- content: vec![ContentPart::text(text)], +- name: None, ++ role: Role::User, ++ content: vec![ContentPart::text(text)], ++ name: None, + tool_call_id: None, + } + } + + pub fn assistant(text: impl Into) -> Self { + Self { +- role: Role::Assistant, +- content: vec![ContentPart::text(text)], +- name: None, ++ role: Role::Assistant, ++ content: vec![ContentPart::text(text)], ++ name: None, + tool_call_id: None, + } + } +@@ -294,15 +294,15 @@ impl Message { + ) -> Self { + let id = tool_call_id.into(); + Self { +- role: Role::Tool, +- content: vec![ContentPart::ToolResult(ToolResult { ++ role: Role::Tool, ++ content: vec![ContentPart::ToolResult(ToolResult { + tool_call_id: id.clone(), + content, + is_error, + image_data: None, + image_media_type: None, + })], +- name: None, ++ name: None, + tool_call_id: Some(id), + } + } +@@ -384,10 +384,10 @@ pub enum ResponseFormatType { + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct ResponseFormat { + #[serde(rename = "type")] +- pub kind: ResponseFormatType, ++ pub kind: ResponseFormatType, + pub json_schema: Option, + #[serde(default)] +- pub strict: bool, ++ pub strict: bool, + } + + // --- 3.11 Warning --- +@@ -395,7 +395,7 @@ pub struct ResponseFormat { + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct Warning { + pub message: String, +- pub code: Option, ++ pub code: Option, + } + + // --- 3.12 RateLimitInfo --- +@@ -403,10 +403,10 @@ pub struct Warning { + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct RateLimitInfo { + pub requests_remaining: Option, +- pub requests_limit: Option, +- pub tokens_remaining: Option, +- pub tokens_limit: Option, +- pub reset_at: Option, ++ pub requests_limit: Option, ++ pub tokens_remaining: Option, ++ pub tokens_limit: Option, ++ pub reset_at: Option, + } + + // --- 3.8 ReasoningEffort --- +@@ -459,19 +459,19 @@ impl std::str::FromStr for ReasoningEffort { + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Request { +- pub model: String, +- pub messages: Vec, +- pub provider: Option, +- pub tools: Option>, +- pub tool_choice: Option, +- pub response_format: Option, +- pub temperature: Option, +- pub top_p: Option, +- pub max_tokens: Option, +- pub stop_sequences: Option>, ++ pub model: String, ++ pub messages: Vec, ++ pub provider: Option, ++ pub tools: Option>, ++ pub tool_choice: Option, ++ pub response_format: Option, ++ pub temperature: Option, ++ pub top_p: Option, ++ pub max_tokens: Option, ++ pub stop_sequences: Option>, + pub reasoning_effort: Option, +- pub speed: Option, +- pub metadata: Option>, ++ pub speed: Option, ++ pub metadata: Option>, + pub provider_options: Option, + } + +@@ -479,9 +479,9 @@ pub struct Request { + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ToolDefinition { +- pub name: String, ++ pub name: String, + pub description: String, +- pub parameters: serde_json::Value, ++ pub parameters: serde_json::Value, + } + + // --- 5.3 ToolChoice --- +@@ -518,15 +518,15 @@ impl ToolChoice { + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Response { +- pub id: String, +- pub model: String, +- pub provider: String, +- pub message: Message, ++ pub id: String, ++ pub model: String, ++ pub provider: String, ++ pub message: Message, + pub finish_reason: FinishReason, +- pub usage: TokenCounts, +- pub raw: Option, +- pub warnings: Vec, +- pub rate_limit: Option, ++ pub usage: TokenCounts, ++ pub raw: Option, ++ pub warnings: Vec, ++ pub rate_limit: Option, + } + + impl Response { +@@ -577,7 +577,7 @@ pub enum StreamEvent { + text_id: Option, + }, + TextDelta { +- delta: String, ++ delta: String, + text_id: Option, + }, + TextEnd { +@@ -599,19 +599,19 @@ pub enum StreamEvent { + }, + StepFinish { + finish_reason: FinishReason, +- usage: TokenCounts, +- response: Box, +- tool_calls: Vec, +- tool_results: Vec, ++ usage: TokenCounts, ++ response: Box, ++ tool_calls: Vec, ++ tool_results: Vec, + }, + Finish { + finish_reason: FinishReason, +- usage: TokenCounts, +- response: Box, ++ usage: TokenCounts, ++ response: Box, + }, + Error { + error: Error, +- raw: Option, ++ raw: Option, + }, + } + +@@ -663,14 +663,14 @@ pub use fabro_model::{Model, ModelCosts, ModelFeatures, ModelLimits}; + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct TimeoutOptions { +- pub total: Option, ++ pub total: Option, + pub per_step: Option, + } + + impl From for TimeoutOptions { + fn from(total: f64) -> Self { + Self { +- total: Some(total), ++ total: Some(total), + per_step: None, + } + } +@@ -678,16 +678,16 @@ impl From for TimeoutOptions { + + #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] + pub struct AdapterTimeout { +- pub connect: f64, +- pub request: Option, ++ pub connect: f64, ++ pub request: Option, + pub stream_read: Option, + } + + impl Default for AdapterTimeout { + fn default() -> Self { + Self { +- connect: 30.0, +- request: None, ++ connect: 30.0, ++ request: None, + stream_read: Some(300.0), + } + } +@@ -702,9 +702,9 @@ pub type OnRetryCallback = Arc, ++ pub on_retry: Option, + } + + impl std::fmt::Debug for RetryPolicy { +@@ -721,13 +721,13 @@ impl Default for RetryPolicy { + fn default() -> Self { + Self { + max_retries: 2, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: std::time::Duration::from_secs(1), +- factor: 2.0, +- max_delay: std::time::Duration::from_secs(60), +- jitter: true, ++ factor: 2.0, ++ max_delay: std::time::Duration::from_secs(60), ++ jitter: true, + }, +- on_retry: None, ++ on_retry: None, + } + } + } +@@ -743,7 +743,7 @@ pub enum ObjectStreamEvent { + Delta { event: StreamEvent }, + /// The stream completed with a fully parsed object and response. + Complete { +- object: serde_json::Value, ++ object: serde_json::Value, + response: Box, + }, + } +@@ -752,11 +752,11 @@ pub enum ObjectStreamEvent { + + #[derive(Debug, Clone)] + pub struct GenerateResult { +- pub response: Response, ++ pub response: Response, + pub tool_results: Vec, +- pub total_usage: TokenCounts, +- pub steps: Vec, +- pub output: Option, ++ pub total_usage: TokenCounts, ++ pub steps: Vec, ++ pub output: Option, + } + + impl std::ops::Deref for GenerateResult { +@@ -768,7 +768,7 @@ impl std::ops::Deref for GenerateResult { + + #[derive(Debug, Clone)] + pub struct StepResult { +- pub response: Response, ++ pub response: Response, + pub tool_results: Vec, + } + +@@ -827,13 +827,13 @@ mod tests { + #[test] + fn message_text_concatenates_text_parts() { + let msg = Message { +- role: Role::Assistant, +- content: vec![ ++ role: Role::Assistant, ++ content: vec![ + ContentPart::text("Hello "), + ContentPart::ToolCall(ToolCall::new("c1", "test", serde_json::json!({}))), + ContentPart::text("world"), + ], +- name: None, ++ name: None, + tool_call_id: None, + }; + assert_eq!(msg.text(), "Hello world"); +@@ -842,13 +842,13 @@ mod tests { + #[test] + fn message_text_returns_empty_for_no_text_parts() { + let msg = Message { +- role: Role::Assistant, +- content: vec![ContentPart::ToolCall(ToolCall::new( ++ role: Role::Assistant, ++ content: vec![ContentPart::ToolCall(ToolCall::new( + "c1", + "test", + serde_json::json!({}), + ))], +- name: None, ++ name: None, + tool_call_id: None, + }; + assert_eq!(msg.text(), ""); +@@ -903,10 +903,10 @@ mod tests { + #[test] + fn usage_serialization_includes_present_optional_fields() { + let usage = TokenCounts { +- input_tokens: 100, +- output_tokens: 30, +- reasoning_tokens: 20, +- cache_read_tokens: 80, ++ input_tokens: 100, ++ output_tokens: 30, ++ reasoning_tokens: 20, ++ cache_read_tokens: 80, + cache_write_tokens: 10, + }; + insta::assert_snapshot!(serde_json::to_string_pretty(&usage).unwrap(), @r#" +@@ -933,17 +933,17 @@ mod tests { + #[test] + fn usage_addition_both_filled() { + let a = TokenCounts { +- input_tokens: 10, +- output_tokens: 15, +- reasoning_tokens: 5, +- cache_read_tokens: 3, ++ input_tokens: 10, ++ output_tokens: 15, ++ reasoning_tokens: 5, ++ cache_read_tokens: 3, + cache_write_tokens: 1, + }; + let b = TokenCounts { +- input_tokens: 15, +- output_tokens: 15, +- reasoning_tokens: 10, +- cache_read_tokens: 7, ++ input_tokens: 15, ++ output_tokens: 15, ++ reasoning_tokens: 10, ++ cache_read_tokens: 7, + cache_write_tokens: 2, + }; + let sum = a + b; +@@ -981,23 +981,26 @@ mod tests { + assert_eq!(ToolChoice::None, ToolChoice::None); + assert_eq!(ToolChoice::Required, ToolChoice::Required); + let named = ToolChoice::named("get_weather"); +- assert_eq!(named, ToolChoice::Named { +- tool_name: "get_weather".to_string(), +- }); ++ assert_eq!( ++ named, ++ ToolChoice::Named { ++ tool_name: "get_weather".to_string(), ++ } ++ ); + } + + #[test] + fn response_text_accessor() { + let response = Response { +- id: "resp_1".into(), +- model: "test-model".into(), +- provider: "test".into(), +- message: Message::assistant("Hello world"), ++ id: "resp_1".into(), ++ model: "test-model".into(), ++ provider: "test".into(), ++ message: Message::assistant("Hello world"), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + assert_eq!(response.text(), "Hello world"); + } +@@ -1005,12 +1008,12 @@ mod tests { + #[test] + fn response_tool_calls_accessor() { + let response = Response { +- id: "resp_1".into(), +- model: "test-model".into(), +- provider: "test".into(), +- message: Message { +- role: Role::Assistant, +- content: vec![ ++ id: "resp_1".into(), ++ model: "test-model".into(), ++ provider: "test".into(), ++ message: Message { ++ role: Role::Assistant, ++ content: vec![ + ContentPart::text("Let me check"), + ContentPart::ToolCall(ToolCall::new( + "call_1", +@@ -1018,14 +1021,14 @@ mod tests { + serde_json::json!({"city": "SF"}), + )), + ], +- name: None, ++ name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + let calls = response.tool_calls(); + assert_eq!(calls.len(), 1); +@@ -1036,27 +1039,27 @@ mod tests { + #[test] + fn response_reasoning_accessor() { + let response = Response { +- id: "resp_1".into(), +- model: "test-model".into(), +- provider: "test".into(), +- message: Message { +- role: Role::Assistant, +- content: vec![ ++ id: "resp_1".into(), ++ model: "test-model".into(), ++ provider: "test".into(), ++ message: Message { ++ role: Role::Assistant, ++ content: vec![ + ContentPart::Thinking(ThinkingData { +- text: "Let me think...".into(), ++ text: "Let me think...".into(), + signature: Some("sig_123".into()), +- redacted: false, ++ redacted: false, + }), + ContentPart::text("The answer is 42."), + ], +- name: None, ++ name: None, + tool_call_id: None, + }, + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + assert_eq!(response.reasoning(), Some("Let me think...".to_string())); + assert_eq!(response.text(), "The answer is 42."); +@@ -1065,15 +1068,15 @@ mod tests { + #[test] + fn response_reasoning_returns_none_when_absent() { + let response = Response { +- id: "resp_1".into(), +- model: "test-model".into(), +- provider: "test".into(), +- message: Message::assistant("Hello"), ++ id: "resp_1".into(), ++ model: "test-model".into(), ++ provider: "test".into(), ++ message: Message::assistant("Hello"), + finish_reason: FinishReason::Stop, +- usage: TokenCounts::default(), +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ usage: TokenCounts::default(), ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + assert_eq!(response.reasoning(), None); + } +@@ -1094,7 +1097,7 @@ mod tests { + fn stream_event_error() { + let event = StreamEvent::error(Error::Stream { + message: "something went wrong".into(), +- source: None, ++ source: None, + }); + match &event { + StreamEvent::Error { error, .. } => { +@@ -1111,9 +1114,9 @@ mod tests { + max_retries: 3, + backoff: BackoffPolicy { + initial_delay: Duration::from_secs(1), +- factor: 2.0, +- max_delay: Duration::from_secs(60), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(60), ++ jitter: false, + }, + ..Default::default() + }; +@@ -1131,9 +1134,9 @@ mod tests { + max_retries: 10, + backoff: BackoffPolicy { + initial_delay: Duration::from_secs(1), +- factor: 2.0, +- max_delay: Duration::from_secs(5), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(5), ++ jitter: false, + }, + ..Default::default() + }; +@@ -1147,9 +1150,9 @@ mod tests { + max_retries: 3, + backoff: BackoffPolicy { + initial_delay: Duration::from_secs(1), +- factor: 2.0, +- max_delay: Duration::from_secs(60), +- jitter: true, ++ factor: 2.0, ++ max_delay: Duration::from_secs(60), ++ jitter: true, + }, + ..Default::default() + }; +@@ -1176,10 +1179,10 @@ mod tests { + #[test] + fn content_part_image_constructor() { + let part = ContentPart::Image(ImageData { +- url: Some("https://example.com/img.png".into()), +- data: None, ++ url: Some("https://example.com/img.png".into()), ++ data: None, + media_type: None, +- detail: None, ++ detail: None, + }); + assert!(matches!(part, ContentPart::Image(_))); + } +@@ -1195,10 +1198,10 @@ mod tests { + #[test] + fn tool_result_with_image_data() { + let result = ToolResult { +- tool_call_id: "call_1".into(), +- content: serde_json::json!("screenshot taken"), +- is_error: false, +- image_data: Some(vec![0x89, 0x50, 0x4E, 0x47]), ++ tool_call_id: "call_1".into(), ++ content: serde_json::json!("screenshot taken"), ++ is_error: false, ++ image_data: Some(vec![0x89, 0x50, 0x4E, 0x47]), + image_media_type: Some("image/png".into()), + }; + assert!(result.image_data.is_some()); +@@ -1231,19 +1234,19 @@ mod tests { + #[test] + fn stream_event_step_finish_constructor() { + let response = Response { +- id: "resp_1".into(), +- model: "test-model".into(), +- provider: "test".into(), +- message: Message::assistant("tool response"), ++ id: "resp_1".into(), ++ model: "test-model".into(), ++ provider: "test".into(), ++ message: Message::assistant("tool response"), + finish_reason: FinishReason::ToolCalls, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 5, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }; + let tool_calls = vec![ToolCall::new( + "call_1", +diff --git a/lib/crates/fabro-llm/tests/integration.rs b/lib/crates/fabro-llm/tests/integration.rs +index 28de85a8..36324633 100644 +--- a/lib/crates/fabro-llm/tests/integration.rs ++++ b/lib/crates/fabro-llm/tests/integration.rs +@@ -5,19 +5,19 @@ use fabro_llm::types::{FinishReason, Message, Request}; + + fn make_request(model: &str) -> Request { + Request { +- model: model.to_string(), +- messages: vec![Message::user("Say hello in exactly one word")], +- provider: None, +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: Some(0.0), +- top_p: None, +- max_tokens: Some(50), +- stop_sequences: None, ++ model: model.to_string(), ++ messages: vec![Message::user("Say hello in exactly one word")], ++ provider: None, ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: Some(0.0), ++ top_p: None, ++ max_tokens: Some(50), ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + } + } +@@ -157,19 +157,19 @@ async fn run_multi_turn_cache_test( + + for turn in 0..6 { + let request = Request { +- model: model.to_string(), +- messages: messages.clone(), +- provider: None, +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: Some(0.0), +- top_p: None, +- max_tokens: Some(100), +- stop_sequences: None, ++ model: model.to_string(), ++ messages: messages.clone(), ++ provider: None, ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: Some(0.0), ++ top_p: None, ++ max_tokens: Some(100), ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + }; + +diff --git a/lib/crates/fabro-mcp/src/client.rs b/lib/crates/fabro-mcp/src/client.rs +index 1c2cfde0..7d0da563 100644 +--- a/lib/crates/fabro-mcp/src/client.rs ++++ b/lib/crates/fabro-mcp/src/client.rs +@@ -32,7 +32,7 @@ enum PendingTransport { + /// MCP client wrapping the rmcp SDK. Handles stdio and HTTP transports. + pub struct McpClient { + server_name: String, +- state: Mutex, ++ state: Mutex, + } + + impl McpClient { +@@ -101,7 +101,7 @@ impl McpClient { + + Ok(Self { + server_name: config.name.clone(), +- state: Mutex::new(ClientState::Connecting(Some(transport))), ++ state: Mutex::new(ClientState::Connecting(Some(transport))), + }) + } + +diff --git a/lib/crates/fabro-mcp/src/connection_manager.rs b/lib/crates/fabro-mcp/src/connection_manager.rs +index 59e12f6c..b436e6ce 100644 +--- a/lib/crates/fabro-mcp/src/connection_manager.rs ++++ b/lib/crates/fabro-mcp/src/connection_manager.rs +@@ -78,16 +78,16 @@ pub fn call_result_to_string(result: &CallToolResult) -> Result + /// Tool info stored per-tool in the connection manager. + #[derive(Debug, Clone)] + pub struct ToolInfo { +- pub server_name: String, ++ pub server_name: String, + pub original_tool_name: String, +- pub description: String, +- pub input_schema: serde_json::Value, ++ pub description: String, ++ pub input_schema: serde_json::Value, + } + + /// Manages connections to multiple MCP servers and their tools. + pub struct McpConnectionManager { + clients: HashMap>, +- tools: HashMap, ++ tools: HashMap, + } + + impl McpConnectionManager { +@@ -95,7 +95,7 @@ impl McpConnectionManager { + pub fn new() -> Self { + Self { + clients: HashMap::new(), +- tools: HashMap::new(), ++ tools: HashMap::new(), + } + } + +@@ -132,12 +132,15 @@ impl McpConnectionManager { + + for (name, description, input_schema) in tools { + let qualified = qualified_tool_name(&config.name, &name); +- self.tools.insert(qualified, ToolInfo { +- server_name: config.name.clone(), +- original_tool_name: name, +- description, +- input_schema, +- }); ++ self.tools.insert( ++ qualified, ++ ToolInfo { ++ server_name: config.name.clone(), ++ original_tool_name: name, ++ description, ++ input_schema, ++ }, ++ ); + } + + self.clients.insert(config.name.clone(), Arc::new(client)); +diff --git a/lib/crates/fabro-mcp/tests/stdio_integration.rs b/lib/crates/fabro-mcp/tests/stdio_integration.rs +index 8da5c24d..b07d0f1a 100644 +--- a/lib/crates/fabro-mcp/tests/stdio_integration.rs ++++ b/lib/crates/fabro-mcp/tests/stdio_integration.rs +@@ -8,13 +8,13 @@ use fabro_mcp::connection_manager::{McpConnectionManager, call_result_to_string} + fn test_server_config() -> McpServerSettings { + let test_server = format!("{}/tests/test_mcp_server.py", env!("CARGO_MANIFEST_DIR")); + McpServerSettings { +- name: "test-echo".into(), +- transport: McpTransport::Stdio { ++ name: "test-echo".into(), ++ transport: McpTransport::Stdio { + command: vec!["python3".into(), test_server], +- env: HashMap::new(), ++ env: HashMap::new(), + }, + startup_timeout_secs: 10, +- tool_timeout_secs: 30, ++ tool_timeout_secs: 30, + } + } + +diff --git a/lib/crates/fabro-model/src/billing.rs b/lib/crates/fabro-model/src/billing.rs +index 6ba62b08..07a5e65d 100644 +--- a/lib/crates/fabro-model/src/billing.rs ++++ b/lib/crates/fabro-model/src/billing.rs +@@ -132,17 +132,17 @@ pub struct ModelRef { + pub provider: Provider, + pub model_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub speed: Option, ++ pub speed: Option, + } + + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] + pub struct TokenCounts { +- pub input_tokens: i64, +- pub output_tokens: i64, ++ pub input_tokens: i64, ++ pub output_tokens: i64, + #[serde(default)] +- pub reasoning_tokens: i64, ++ pub reasoning_tokens: i64, + #[serde(default)] +- pub cache_read_tokens: i64, ++ pub cache_read_tokens: i64, + #[serde(default)] + pub cache_write_tokens: i64, + } +@@ -167,10 +167,10 @@ impl std::ops::Add for TokenCounts { + + fn add(self, rhs: Self) -> Self::Output { + Self { +- input_tokens: self.input_tokens + rhs.input_tokens, +- output_tokens: self.output_tokens + rhs.output_tokens, +- reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens, +- cache_read_tokens: self.cache_read_tokens + rhs.cache_read_tokens, ++ input_tokens: self.input_tokens + rhs.input_tokens, ++ output_tokens: self.output_tokens + rhs.output_tokens, ++ reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens, ++ cache_read_tokens: self.cache_read_tokens + rhs.cache_read_tokens, + cache_write_tokens: self.cache_write_tokens + rhs.cache_write_tokens, + } + } +@@ -188,30 +188,30 @@ impl std::ops::AddAssign for TokenCounts { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct ModelUsage { +- pub model: ModelRef, ++ pub model: ModelRef, + pub tokens: TokenCounts, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct OpenAiModelPricing { +- pub input: PricePerMTok, ++ pub input: PricePerMTok, + pub cached_input: Option, +- pub output: PricePerMTok, ++ pub output: PricePerMTok, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct AnthropicModelPricing { +- pub input: PricePerMTok, +- pub cache_read: Option, ++ pub input: PricePerMTok, ++ pub cache_read: Option, + pub cache_write_5m: Option, + pub cache_write_1h: Option, +- pub output: PricePerMTok, ++ pub output: PricePerMTok, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct GeminiStorageSegment { + pub cached_tokens: i64, +- pub ttl_seconds: i64, ++ pub ttl_seconds: i64, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +@@ -221,11 +221,11 @@ pub struct GeminiStoragePricing { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct GeminiModelPricing { +- pub input: PricePerMTok, +- pub output: PricePerMTok, ++ pub input: PricePerMTok, ++ pub output: PricePerMTok, + pub cached_input: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub storage: Option, ++ pub storage: Option, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +@@ -243,7 +243,7 @@ pub enum ModelPricingPolicy { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct ModelPricing { +- pub model: ModelRef, ++ pub model: ModelRef, + pub policy: ModelPricingPolicy, + } + +@@ -302,7 +302,7 @@ pub struct ModelBillingInput { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct BilledModelUsage { +- pub input: ModelBillingInput, ++ pub input: ModelBillingInput, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_usd_micros: Option, + } +@@ -326,17 +326,17 @@ impl BilledModelUsage { + + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] + pub struct BilledTokenCounts { +- pub input_tokens: i64, +- pub output_tokens: i64, +- pub total_tokens: i64, ++ pub input_tokens: i64, ++ pub output_tokens: i64, ++ pub total_tokens: i64, + #[serde(default)] +- pub reasoning_tokens: i64, ++ pub reasoning_tokens: i64, + #[serde(default)] +- pub cache_read_tokens: i64, ++ pub cache_read_tokens: i64, + #[serde(default)] + pub cache_write_tokens: i64, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub total_usd_micros: Option, ++ pub total_usd_micros: Option, + } + + impl BilledTokenCounts { +@@ -355,13 +355,13 @@ impl BilledTokenCounts { + } + + Self { +- input_tokens: tokens.input_tokens, +- output_tokens: tokens.output_tokens, +- total_tokens: tokens.total_tokens(), +- reasoning_tokens: tokens.reasoning_tokens, +- cache_read_tokens: tokens.cache_read_tokens, ++ input_tokens: tokens.input_tokens, ++ output_tokens: tokens.output_tokens, ++ total_tokens: tokens.total_tokens(), ++ reasoning_tokens: tokens.reasoning_tokens, ++ cache_read_tokens: tokens.cache_read_tokens, + cache_write_tokens: tokens.cache_write_tokens, +- total_usd_micros: has_total.then_some(total_usd_micros), ++ total_usd_micros: has_total.then_some(total_usd_micros), + } + } + } +@@ -587,31 +587,31 @@ mod tests { + #[test] + fn openai_pricing_bills_cached_input_and_reasoning_output() { + let pricing = ModelPricing { +- model: ModelRef { ++ model: ModelRef { + provider: Provider::OpenAi, + model_id: "gpt-5.4".to_string(), +- speed: None, ++ speed: None, + }, + policy: ModelPricingPolicy::OpenAi(OpenAiModelPricing { +- input: PricePerMTok { ++ input: PricePerMTok { + usd_micros: 1_250_000, + }, + cached_input: Some(PricePerMTok { + usd_micros: 125_000, + }), +- output: PricePerMTok { ++ output: PricePerMTok { + usd_micros: 10_000_000, + }, + }), + }; + let input = ModelBillingInput { + usage: ModelUsage { +- model: pricing.model.clone(), ++ model: pricing.model.clone(), + tokens: TokenCounts { +- input_tokens: 500_000, +- output_tokens: 125_000, +- reasoning_tokens: 25_000, +- cache_read_tokens: 250_000, ++ input_tokens: 500_000, ++ output_tokens: 125_000, ++ reasoning_tokens: 25_000, ++ cache_read_tokens: 250_000, + cache_write_tokens: 0, + }, + }, +@@ -640,16 +640,16 @@ mod tests { + #[test] + fn anthropic_billing_supports_distinct_cache_write_buckets() { + let pricing = ModelPricing { +- model: ModelRef { ++ model: ModelRef { + provider: Provider::Anthropic, + model_id: "claude-opus-4-6".to_string(), +- speed: Some(Speed::Fast), ++ speed: Some(Speed::Fast), + }, + policy: ModelPricingPolicy::Anthropic(AnthropicModelPricing { +- input: PricePerMTok { ++ input: PricePerMTok { + usd_micros: 30_000_000, + }, +- cache_read: Some(PricePerMTok { ++ cache_read: Some(PricePerMTok { + usd_micros: 3_000_000, + }), + cache_write_5m: Some(PricePerMTok { +@@ -658,19 +658,19 @@ mod tests { + cache_write_1h: Some(PricePerMTok { + usd_micros: 60_000_000, + }), +- output: PricePerMTok { ++ output: PricePerMTok { + usd_micros: 150_000_000, + }, + }), + }; + let input = ModelBillingInput { + usage: ModelUsage { +- model: pricing.model.clone(), ++ model: pricing.model.clone(), + tokens: TokenCounts { +- input_tokens: 100_000, +- output_tokens: 10_000, +- reasoning_tokens: 5_000, +- cache_read_tokens: 20_000, ++ input_tokens: 100_000, ++ output_tokens: 10_000, ++ reasoning_tokens: 5_000, ++ cache_read_tokens: 20_000, + cache_write_tokens: 0, + }, + }, +@@ -686,37 +686,37 @@ mod tests { + #[test] + fn gemini_billing_requires_storage_pricing_when_storage_facts_exist() { + let pricing = ModelPricing { +- model: ModelRef { ++ model: ModelRef { + provider: Provider::Gemini, + model_id: "gemini-3.1-pro-preview".to_string(), +- speed: None, ++ speed: None, + }, + policy: ModelPricingPolicy::Gemini(GeminiModelPricing { +- input: PricePerMTok { ++ input: PricePerMTok { + usd_micros: 1_250_000, + }, +- output: PricePerMTok { ++ output: PricePerMTok { + usd_micros: 10_000_000, + }, + cached_input: None, +- storage: None, ++ storage: None, + }), + }; + let input = ModelBillingInput { + usage: ModelUsage { +- model: pricing.model.clone(), ++ model: pricing.model.clone(), + tokens: TokenCounts { +- input_tokens: 100_000, +- output_tokens: 10_000, +- reasoning_tokens: 0, +- cache_read_tokens: 0, ++ input_tokens: 100_000, ++ output_tokens: 10_000, ++ reasoning_tokens: 0, ++ cache_read_tokens: 0, + cache_write_tokens: 0, + }, + }, + facts: ModelBillingFacts::Gemini(GeminiBillingFacts { + storage_segments: vec![GeminiStorageSegment { + cached_tokens: 100_000, +- ttl_seconds: 60, ++ ttl_seconds: 60, + }], + }), + }; +diff --git a/lib/crates/fabro-model/src/catalog.rs b/lib/crates/fabro-model/src/catalog.rs +index ef96f2b2..bc9332d2 100644 +--- a/lib/crates/fabro-model/src/catalog.rs ++++ b/lib/crates/fabro-model/src/catalog.rs +@@ -15,7 +15,7 @@ static GLOBAL_CATALOG: LazyLock = LazyLock::new(|| { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct FallbackTarget { + pub provider: String, +- pub model: String, ++ pub model: String, + } + + /// Typed model catalog backed by a `Vec`. +@@ -162,7 +162,7 @@ impl Catalog { + let provider = provider_str.parse::().ok()?; + self.closest(provider, reference).map(|m| FallbackTarget { + provider: provider_str.clone(), +- model: m.id.clone(), ++ model: m.id.clone(), + }) + }) + .collect() +@@ -283,10 +283,10 @@ mod tests { + + #[test] + fn builtin_build_fallback_chain() { +- let fallbacks = HashMap::from([("anthropic".to_string(), vec![ +- "gemini".to_string(), +- "openai".to_string(), +- ])]); ++ let fallbacks = HashMap::from([( ++ "anthropic".to_string(), ++ vec!["gemini".to_string(), "openai".to_string()], ++ )]); + let chain = Catalog::builtin().build_fallback_chain( + Provider::Anthropic, + "claude-opus-4-6", +@@ -320,10 +320,10 @@ mod tests { + + #[test] + fn builtin_build_fallback_chain_skips_no_capability_match() { +- let fallbacks = HashMap::from([("anthropic".to_string(), vec![ +- "openai".to_string(), +- "kimi".to_string(), +- ])]); ++ let fallbacks = HashMap::from([( ++ "anthropic".to_string(), ++ vec!["openai".to_string(), "kimi".to_string()], ++ )]); + let chain = Catalog::builtin().build_fallback_chain( + Provider::Anthropic, + "claude-haiku-4-5", +@@ -350,30 +350,30 @@ mod tests { + use crate::types::{Model, ModelCosts, ModelFeatures, ModelLimits}; + + let models = vec![Model { +- id: "test-model".to_string(), +- provider: Provider::Anthropic, +- family: "test".to_string(), +- display_name: "Test Model".to_string(), +- limits: ModelLimits { ++ id: "test-model".to_string(), ++ provider: Provider::Anthropic, ++ family: "test".to_string(), ++ display_name: "Test Model".to_string(), ++ limits: ModelLimits { + context_window: 100_000, +- max_output: Some(4096), ++ max_output: Some(4096), + }, +- training: None, +- knowledge_cutoff: None, +- features: ModelFeatures { +- tools: true, +- vision: false, ++ training: None, ++ knowledge_cutoff: None, ++ features: ModelFeatures { ++ tools: true, ++ vision: false, + reasoning: false, +- effort: false, ++ effort: false, + }, +- costs: ModelCosts { +- input_cost_per_mtok: Some(1.0), +- output_cost_per_mtok: Some(5.0), ++ costs: ModelCosts { ++ input_cost_per_mtok: Some(1.0), ++ output_cost_per_mtok: Some(5.0), + cache_input_cost_per_mtok: None, + }, + estimated_output_tps: None, +- aliases: vec!["test".to_string()], +- default: true, ++ aliases: vec!["test".to_string()], ++ default: true, + }]; + + let catalog = Catalog::from_models(models); +diff --git a/lib/crates/fabro-model/src/model_ref.rs b/lib/crates/fabro-model/src/model_ref.rs +index 96cbced9..38ad13d6 100644 +--- a/lib/crates/fabro-model/src/model_ref.rs ++++ b/lib/crates/fabro-model/src/model_ref.rs +@@ -11,10 +11,7 @@ pub enum ModelHandle { + /// A model whose metadata has been resolved from the catalog. + Resolved(Arc), + /// An unresolved provider:model pair (e.g. from CLI input or config). +- ByName { +- provider: Provider, +- model: String, +- }, ++ ByName { provider: Provider, model: String }, + } + + impl ModelHandle { +@@ -65,7 +62,7 @@ mod tests { + fn by_name_display() { + let r = ModelHandle::ByName { + provider: Provider::Anthropic, +- model: "claude-opus-4-6".to_string(), ++ model: "claude-opus-4-6".to_string(), + }; + assert_eq!(r.to_string(), "anthropic:claude-opus-4-6"); + } +@@ -74,7 +71,7 @@ mod tests { + fn by_name_accessors() { + let r = ModelHandle::ByName { + provider: Provider::OpenAi, +- model: "gpt-5.4".to_string(), ++ model: "gpt-5.4".to_string(), + }; + assert_eq!(r.model_id(), "gpt-5.4"); + assert_eq!(r.provider(), Provider::OpenAi); +@@ -99,7 +96,7 @@ mod tests { + fn debug_format() { + let r = ModelHandle::ByName { + provider: Provider::Gemini, +- model: "gemini-3.1-pro-preview".to_string(), ++ model: "gemini-3.1-pro-preview".to_string(), + }; + let debug = format!("{r:?}"); + assert!(debug.contains("ByName")); +diff --git a/lib/crates/fabro-model/src/provider.rs b/lib/crates/fabro-model/src/provider.rs +index 3af18547..2186ca19 100644 +--- a/lib/crates/fabro-model/src/provider.rs ++++ b/lib/crates/fabro-model/src/provider.rs +@@ -246,9 +246,10 @@ mod tests { + + #[test] + fn api_key_env_vars_anthropic() { +- assert_eq!(Provider::Anthropic.api_key_env_vars(), &[ +- "ANTHROPIC_API_KEY" +- ]); ++ assert_eq!( ++ Provider::Anthropic.api_key_env_vars(), ++ &["ANTHROPIC_API_KEY"] ++ ); + } + + #[test] +@@ -280,9 +281,10 @@ mod tests { + + #[test] + fn api_key_env_vars_inception() { +- assert_eq!(Provider::Inception.api_key_env_vars(), &[ +- "INCEPTION_API_KEY" +- ]); ++ assert_eq!( ++ Provider::Inception.api_key_env_vars(), ++ &["INCEPTION_API_KEY"] ++ ); + } + + #[test] +diff --git a/lib/crates/fabro-model/src/types.rs b/lib/crates/fabro-model/src/types.rs +index 4eafcdc9..f320533c 100644 +--- a/lib/crates/fabro-model/src/types.rs ++++ b/lib/crates/fabro-model/src/types.rs +@@ -7,13 +7,13 @@ use crate::provider::Provider; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct ModelLimits { + pub context_window: i64, +- pub max_output: Option, ++ pub max_output: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct ModelFeatures { +- pub tools: bool, +- pub vision: bool, ++ pub tools: bool, ++ pub vision: bool, + pub reasoning: bool, + /// Whether the model supports the `reasoning_effort` / `effort` parameter + /// directly (e.g. Anthropic `output_config.effort`, OpenAI +@@ -21,31 +21,31 @@ pub struct ModelFeatures { + /// (e.g. claude-sonnet-4-5) need the older `thinking` API with + /// `budget_tokens` instead. + #[serde(default)] +- pub effort: bool, ++ pub effort: bool, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct ModelCosts { +- pub input_cost_per_mtok: Option, +- pub output_cost_per_mtok: Option, ++ pub input_cost_per_mtok: Option, ++ pub output_cost_per_mtok: Option, + pub cache_input_cost_per_mtok: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct Model { +- pub id: String, +- pub provider: Provider, +- pub family: String, +- pub display_name: String, +- pub limits: ModelLimits, +- pub training: Option, +- pub knowledge_cutoff: Option, +- pub features: ModelFeatures, +- pub costs: ModelCosts, ++ pub id: String, ++ pub provider: Provider, ++ pub family: String, ++ pub display_name: String, ++ pub limits: ModelLimits, ++ pub training: Option, ++ pub knowledge_cutoff: Option, ++ pub features: ModelFeatures, ++ pub costs: ModelCosts, + pub estimated_output_tps: Option, +- pub aliases: Vec, ++ pub aliases: Vec, + #[serde(default)] +- pub default: bool, ++ pub default: bool, + } + + impl Model { +diff --git a/lib/crates/fabro-oauth/src/lib.rs b/lib/crates/fabro-oauth/src/lib.rs +index 08dbd574..da08b545 100644 +--- a/lib/crates/fabro-oauth/src/lib.rs ++++ b/lib/crates/fabro-oauth/src/lib.rs +@@ -16,7 +16,7 @@ use tokio::sync::oneshot; + // --------------------------------------------------------------------------- + + pub struct PkceCodes { +- pub verifier: String, ++ pub verifier: String, + pub challenge: String, + } + +@@ -101,10 +101,10 @@ pub struct OAuthEndpoint<'a> { + + #[derive(Debug, Deserialize)] + pub struct TokenResponse { +- pub id_token: Option, +- pub access_token: String, ++ pub id_token: Option, ++ pub access_token: String, + pub refresh_token: Option, +- pub expires_in: Option, ++ pub expires_in: Option, + } + + // --------------------------------------------------------------------------- +@@ -207,9 +207,9 @@ pub async fn refresh_token( + + #[derive(Deserialize)] + struct CallbackParams { +- code: Option, +- state: String, +- error: Option, ++ code: Option, ++ state: String, ++ error: Option, + error_description: Option, + } + +diff --git a/lib/crates/fabro-proc/src/title.rs b/lib/crates/fabro-proc/src/title.rs +index f1f08763..e9dafa81 100644 +--- a/lib/crates/fabro-proc/src/title.rs ++++ b/lib/crates/fabro-proc/src/title.rs +@@ -2,7 +2,7 @@ use std::sync::{Mutex, OnceLock}; + + struct Buffer { + start: *mut u8, +- len: usize, ++ len: usize, + } + + // Safety: after init() we treat the captured argv region as exclusively +diff --git a/lib/crates/fabro-retro/src/retro.rs b/lib/crates/fabro-retro/src/retro.rs +index 13011a93..3e1e112d 100644 +--- a/lib/crates/fabro-retro/src/retro.rs ++++ b/lib/crates/fabro-retro/src/retro.rs +@@ -8,15 +8,15 @@ pub use fabro_types::retro::{ + + #[derive(Debug, Clone)] + pub struct CompletedStage { +- pub node_id: String, +- pub status: String, +- pub succeeded: bool, +- pub failed: bool, +- pub retries: u32, ++ pub node_id: String, ++ pub status: String, ++ pub succeeded: bool, ++ pub failed: bool, ++ pub retries: u32, + pub billing_usd_micros: Option, +- pub notes: Option, +- pub failure_reason: Option, +- pub files_touched: Vec, ++ pub notes: Option, ++ pub failure_reason: Option, ++ pub files_touched: Vec, + } + + pub fn derive_retro( +@@ -51,15 +51,15 @@ pub fn derive_retro( + let dur = stage_durations.get(&cs.node_id).copied().unwrap_or(0); + + stages.push(StageRetro { +- stage_label: cs.node_id.clone(), +- duration_ms: dur, +- retries: cs.retries, ++ stage_label: cs.node_id.clone(), ++ duration_ms: dur, ++ retries: cs.retries, + billing_usd_micros: cs.billing_usd_micros, +- stage_id: cs.node_id, +- status: cs.status, +- notes: cs.notes, +- failure_reason: cs.failure_reason, +- files_touched: cs.files_touched, ++ stage_id: cs.node_id, ++ status: cs.status, ++ notes: cs.notes, ++ failure_reason: cs.failure_reason, ++ files_touched: cs.files_touched, + }); + + all_files.extend( +diff --git a/lib/crates/fabro-retro/src/retro_agent.rs b/lib/crates/fabro-retro/src/retro_agent.rs +index e0a08e27..b5c0643e 100644 +--- a/lib/crates/fabro-retro/src/retro_agent.rs ++++ b/lib/crates/fabro-retro/src/retro_agent.rs +@@ -116,7 +116,7 @@ pub const RETRO_DATA_DIR: &str = "/tmp/retro_data"; + + pub struct RetroAgentResult { + pub narrative: RetroNarrative, +- pub response: String, ++ pub response: String, + } + + #[must_use] +@@ -256,12 +256,12 @@ pub async fn run_retro_agent( + /// derive → apply_narrative → save path without making LLM calls. + pub fn dry_run_narrative() -> RetroNarrative { + RetroNarrative { +- smoothness: SmoothnessRating::Smooth, +- intent: "[dry-run] No LLM analysis performed".to_string(), +- outcome: "[dry-run] Run completed in simulated mode".to_string(), +- learnings: vec![], ++ smoothness: SmoothnessRating::Smooth, ++ intent: "[dry-run] No LLM analysis performed".to_string(), ++ outcome: "[dry-run] Run completed in simulated mode".to_string(), ++ learnings: vec![], + friction_points: vec![], +- open_items: vec![], ++ open_items: vec![], + } + } + +diff --git a/lib/crates/fabro-sandbox/src/config.rs b/lib/crates/fabro-sandbox/src/config.rs +index 4f5ed268..f5707488 100644 +--- a/lib/crates/fabro-sandbox/src/config.rs ++++ b/lib/crates/fabro-sandbox/src/config.rs +@@ -16,11 +16,11 @@ use serde::{Deserialize, Serialize}; + #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] + pub struct DaytonaSettings { + pub auto_stop_interval: Option, +- pub labels: Option>, +- pub snapshot: Option, +- pub network: Option, ++ pub labels: Option>, ++ pub snapshot: Option, ++ pub network: Option, + #[serde(default)] +- pub skip_clone: bool, ++ pub skip_clone: bool, + } + + #[derive(Clone, Debug, PartialEq)] +@@ -117,10 +117,10 @@ pub enum DockerfileSource { + + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + pub struct DaytonaSnapshotSettings { +- pub name: String, +- pub cpu: Option, +- pub memory: Option, +- pub disk: Option, ++ pub name: String, ++ pub cpu: Option, ++ pub memory: Option, ++ pub disk: Option, + pub dockerfile: Option, + } + +diff --git a/lib/crates/fabro-sandbox/src/daytona/mod.rs b/lib/crates/fabro-sandbox/src/daytona/mod.rs +index 6f0f8f00..78cfa7ee 100644 +--- a/lib/crates/fabro-sandbox/src/daytona/mod.rs ++++ b/lib/crates/fabro-sandbox/src/daytona/mod.rs +@@ -41,20 +41,20 @@ async fn build_daytona_client( + + /// Sandbox that runs all operations inside a Daytona cloud sandbox. + pub struct DaytonaSandbox { +- config: DaytonaConfig, +- client: daytona_sdk::Client, +- github_app: Option, +- sandbox: OnceCell, +- rg_available: OnceCell, ++ config: DaytonaConfig, ++ client: daytona_sdk::Client, ++ github_app: Option, ++ sandbox: OnceCell, ++ rg_available: OnceCell, + event_callback: Option, + /// HTTPS origin URL stored after clone so we can refresh push credentials + /// later. +- origin_url: OnceCell, +- run_id: Option, ++ origin_url: OnceCell, ++ run_id: Option, + /// Explicit branch to clone. When set, overrides the branch detected by + /// `detect_repo_info` — avoids cloning a local-only worktree branch + /// (e.g. `fabro/run/...`) that was never pushed to origin. +- clone_branch: Option, ++ clone_branch: Option, + } + + impl DaytonaSandbox { +@@ -248,11 +248,11 @@ impl DaytonaSandbox { + }; + + let params = daytona_sdk::CreateSnapshotParams { +- name: snap_cfg.name.clone(), +- image: daytona_sdk::ImageSource::Custom( ++ name: snap_cfg.name.clone(), ++ image: daytona_sdk::ImageSource::Custom( + daytona_sdk::DockerImage::from_dockerfile(dockerfile), + ), +- resources: Some(daytona_sdk::Resources { ++ resources: Some(daytona_sdk::Resources { + cpu: snap_cfg.cpu, + memory: snap_cfg.memory, + disk: snap_cfg.disk, +@@ -319,7 +319,7 @@ use fabro_github::ssh_url_to_https; + #[derive(Clone, Debug)] + pub struct GitCloneParams { + /// Clean HTTPS URL (no embedded credentials). +- pub url: String, ++ pub url: String, + /// Branch to clone. If None, uses the remote's default. + pub branch: Option, + } +@@ -443,7 +443,7 @@ impl Sandbox for DaytonaSandbox { + let snap_start = Instant::now(); + if let Err(e) = self.ensure_snapshot(snap_cfg).await { + self.emit(SandboxEvent::SnapshotFailed { +- name: snap_cfg.name.clone(), ++ name: snap_cfg.name.clone(), + error: e.clone(), + }); + let duration_ms = +@@ -457,17 +457,17 @@ impl Sandbox for DaytonaSandbox { + } + let snap_duration = u64::try_from(snap_start.elapsed().as_millis()).unwrap_or(u64::MAX); + self.emit(SandboxEvent::SnapshotReady { +- name: snap_cfg.name.clone(), ++ name: snap_cfg.name.clone(), + duration_ms: snap_duration, + }); + + daytona_sdk::CreateParams::Snapshot(daytona_sdk::SnapshotParams { +- base: self.base_params(), ++ base: self.base_params(), + snapshot: snap_cfg.name.clone(), + }) + } else { + daytona_sdk::CreateParams::Snapshot(daytona_sdk::SnapshotParams { +- base: self.base_params(), ++ base: self.base_params(), + snapshot: DEFAULT_SNAPSHOT.to_string(), + }) + }; +@@ -509,7 +509,7 @@ impl Sandbox for DaytonaSandbox { + // Daytona clones over HTTPS with token auth, so rewrite SSH URLs. + let url = ssh_url_to_https(&detected_url); + self.emit(SandboxEvent::GitCloneStarted { +- url: url.clone(), ++ url: url.clone(), + branch: branch.clone(), + }); + let clone_start = Instant::now(); +@@ -521,7 +521,7 @@ impl Sandbox for DaytonaSandbox { + .map_err(|e| { + let err = format!("Failed to parse GitHub URL for clone: {e}"); + self.emit(SandboxEvent::GitCloneFailed { +- url: url.clone(), ++ url: url.clone(), + error: err.clone(), + }); + err +@@ -537,7 +537,7 @@ impl Sandbox for DaytonaSandbox { + let err = + format!("Failed to get GitHub App credentials for clone: {e}"); + self.emit(SandboxEvent::GitCloneFailed { +- url: url.clone(), ++ url: url.clone(), + error: err.clone(), + }); + let duration_ms = u64::try_from(init_start.elapsed().as_millis()) +@@ -561,7 +561,7 @@ impl Sandbox for DaytonaSandbox { + Ok(g) => g, + Err(e) => { + self.emit(SandboxEvent::GitCloneFailed { +- url: url.clone(), ++ url: url.clone(), + error: e.clone(), + }); + let duration_ms = +@@ -577,12 +577,16 @@ impl Sandbox for DaytonaSandbox { + + let clone_token = password.clone(); + let clone_result = git_svc +- .clone(&url, WORKING_DIRECTORY, daytona_sdk::GitCloneOptions { +- branch, +- username, +- password, +- ..Default::default() +- }) ++ .clone( ++ &url, ++ WORKING_DIRECTORY, ++ daytona_sdk::GitCloneOptions { ++ branch, ++ username, ++ password, ++ ..Default::default() ++ }, ++ ) + .await; + + match clone_result { +@@ -590,7 +594,7 @@ impl Sandbox for DaytonaSandbox { + let clone_duration = u64::try_from(clone_start.elapsed().as_millis()) + .unwrap_or(u64::MAX); + self.emit(SandboxEvent::GitCloneCompleted { +- url: url.clone(), ++ url: url.clone(), + duration_ms: clone_duration, + }); + +@@ -686,12 +690,12 @@ impl Sandbox for DaytonaSandbox { + + let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); + self.emit(SandboxEvent::Ready { +- provider: "daytona".into(), ++ provider: "daytona".into(), + duration_ms: init_duration, +- name: Some(sandbox_name), +- cpu: Some(sandbox_cpu), +- memory: Some(sandbox_memory), +- url: Some("https://app.daytona.io/dashboard/sandboxes".into()), ++ name: Some(sandbox_name), ++ cpu: Some(sandbox_cpu), ++ memory: Some(sandbox_memory), ++ url: Some("https://app.daytona.io/dashboard/sandboxes".into()), + }); + + Ok(()) +@@ -708,7 +712,7 @@ impl Sandbox for DaytonaSandbox { + let err = format!("Failed to delete Daytona sandbox: {e}"); + self.emit(SandboxEvent::CleanupFailed { + provider: "daytona".into(), +- error: err.clone(), ++ error: err.clone(), + }); + return Err(err); + } +@@ -946,9 +950,9 @@ impl Sandbox for DaytonaSandbox { + Ok(files + .into_iter() + .map(|f| DirEntry { +- name: f.name, ++ name: f.name, + is_dir: f.is_dir, +- size: if f.size > 0 { ++ size: if f.size > 0 { + Some(u64::try_from(f.size).unwrap()) + } else { + None +@@ -984,8 +988,8 @@ impl Sandbox for DaytonaSandbox { + ); + + let options = daytona_sdk::ExecuteCommandOptions { +- cwd: Some(cwd), +- env: env_vars.cloned(), ++ cwd: Some(cwd), ++ env: env_vars.cloned(), + timeout: Some(std::time::Duration::from_millis(timeout_ms)), + }; + +diff --git a/lib/crates/fabro-sandbox/src/docker.rs b/lib/crates/fabro-sandbox/src/docker.rs +index c61730ec..ccea9823 100644 +--- a/lib/crates/fabro-sandbox/src/docker.rs ++++ b/lib/crates/fabro-sandbox/src/docker.rs +@@ -23,37 +23,37 @@ use crate::{ + /// Configuration for a Docker-based sandbox. + pub struct DockerSandboxOptions { + /// Docker image to use. Default: `"fabro-agent:latest"`. +- pub image: String, ++ pub image: String, + /// Host directory to bind-mount into the container. + pub host_working_directory: String, + /// Mount point inside the container. Default: `"/workspace"`. +- pub container_mount_point: String, ++ pub container_mount_point: String, + /// Docker network mode. Default: `Some("bridge")`. +- pub network_mode: Option, ++ pub network_mode: Option, + /// Additional `"host_path:container_path"` bind mounts. +- pub extra_mounts: Vec, ++ pub extra_mounts: Vec, + /// Memory limit in bytes. `None` = unlimited. +- pub memory_limit: Option, ++ pub memory_limit: Option, + /// CPU quota (microseconds per 100ms period). `None` = unlimited. +- pub cpu_quota: Option, ++ pub cpu_quota: Option, + /// Whether to pull the image if not found locally. Default: `true`. +- pub auto_pull: bool, ++ pub auto_pull: bool, + /// Additional `KEY=VALUE` environment variables for the container. +- pub env_vars: Vec, ++ pub env_vars: Vec, + } + + impl Default for DockerSandboxOptions { + fn default() -> Self { + Self { +- image: "fabro-agent:latest".to_string(), ++ image: "fabro-agent:latest".to_string(), + host_working_directory: String::new(), +- container_mount_point: "/workspace".to_string(), +- network_mode: Some("bridge".to_string()), +- extra_mounts: Vec::new(), +- memory_limit: None, +- cpu_quota: None, +- auto_pull: true, +- env_vars: Vec::new(), ++ container_mount_point: "/workspace".to_string(), ++ network_mode: Some("bridge".to_string()), ++ extra_mounts: Vec::new(), ++ memory_limit: None, ++ cpu_quota: None, ++ auto_pull: true, ++ env_vars: Vec::new(), + } + } + } +@@ -64,13 +64,13 @@ impl Default for DockerSandboxOptions { + /// file operations, commands, grep, and glob execute inside the container via + /// `docker exec`. + pub struct DockerSandbox { +- docker: Docker, +- config: DockerSandboxOptions, +- container_id: OnceCell, +- cached_platform: std::sync::OnceLock, ++ docker: Docker, ++ config: DockerSandboxOptions, ++ container_id: OnceCell, ++ cached_platform: std::sync::OnceLock, + cached_os_version: std::sync::OnceLock, +- rg_available: OnceCell, +- event_callback: Option, ++ rg_available: OnceCell, ++ event_callback: Option, + } + + impl DockerSandbox { +@@ -361,7 +361,7 @@ impl Sandbox for DockerSandbox { + } + let pull_duration = u64::try_from(pull_start.elapsed().as_millis()).unwrap_or(u64::MAX); + self.emit(SandboxEvent::SnapshotPulled { +- name: self.config.image.clone(), ++ name: self.config.image.clone(), + duration_ms: pull_duration, + }); + +@@ -432,12 +432,12 @@ impl Sandbox for DockerSandbox { + + let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); + self.emit(SandboxEvent::Ready { +- provider: "docker".into(), ++ provider: "docker".into(), + duration_ms: init_duration, +- name: None, +- cpu: None, +- memory: None, +- url: None, ++ name: None, ++ cpu: None, ++ memory: None, ++ url: None, + }); + + Ok(()) +diff --git a/lib/crates/fabro-sandbox/src/local.rs b/lib/crates/fabro-sandbox/src/local.rs +index 892e258e..77bc1535 100644 +--- a/lib/crates/fabro-sandbox/src/local.rs ++++ b/lib/crates/fabro-sandbox/src/local.rs +@@ -14,8 +14,8 @@ use crate::{ + + pub struct LocalSandbox { + working_directory: PathBuf, +- event_callback: Option, +- rg_available: std::sync::OnceLock, ++ event_callback: Option, ++ rg_available: std::sync::OnceLock, + } + + impl LocalSandbox { +@@ -830,10 +830,14 @@ mod tests { + + let env = LocalSandbox::new(dir.clone()); + let results = env +- .grep("hello", "test.txt", &GrepOptions { +- case_insensitive: true, +- ..Default::default() +- }) ++ .grep( ++ "hello", ++ "test.txt", ++ &GrepOptions { ++ case_insensitive: true, ++ ..Default::default() ++ }, ++ ) + .await + .unwrap(); + +@@ -848,10 +852,14 @@ mod tests { + + let env = LocalSandbox::new(dir.clone()); + let results = env +- .grep("match", "test.txt", &GrepOptions { +- max_results: Some(2), +- ..Default::default() +- }) ++ .grep( ++ "match", ++ "test.txt", ++ &GrepOptions { ++ max_results: Some(2), ++ ..Default::default() ++ }, ++ ) + .await + .unwrap(); + +diff --git a/lib/crates/fabro-sandbox/src/read_guard.rs b/lib/crates/fabro-sandbox/src/read_guard.rs +index 19b65404..910eec84 100644 +--- a/lib/crates/fabro-sandbox/src/read_guard.rs ++++ b/lib/crates/fabro-sandbox/src/read_guard.rs +@@ -13,7 +13,7 @@ use crate::Sandbox; + /// `write_file` or `delete_file` targets an existing file that hasn't been + /// read. Writing to new (non-existent) files is always allowed. + pub struct ReadBeforeWriteSandbox { +- inner: Arc, ++ inner: Arc, + read_set: Mutex>, + } + +diff --git a/lib/crates/fabro-sandbox/src/sandbox.rs b/lib/crates/fabro-sandbox/src/sandbox.rs +index 8b8284fc..50bce545 100644 +--- a/lib/crates/fabro-sandbox/src/sandbox.rs ++++ b/lib/crates/fabro-sandbox/src/sandbox.rs +@@ -12,8 +12,8 @@ const GIT: &str = "git -c maintenance.auto=0 -c gc.auto=0"; + + /// Information returned when a sandbox sets up git for a workflow run. + pub struct GitRunInfo { +- pub base_sha: String, +- pub run_branch: String, ++ pub base_sha: String, ++ pub run_branch: String, + pub base_branch: Option, + } + +@@ -188,28 +188,28 @@ pub enum SandboxEvent { + provider: String, + }, + Ready { +- provider: String, ++ provider: String, + duration_ms: u64, +- name: Option, +- cpu: Option, +- memory: Option, +- url: Option, ++ name: Option, ++ cpu: Option, ++ memory: Option, ++ url: Option, + }, + InitializeFailed { +- provider: String, +- error: String, ++ provider: String, ++ error: String, + duration_ms: u64, + }, + CleanupStarted { + provider: String, + }, + CleanupCompleted { +- provider: String, ++ provider: String, + duration_ms: u64, + }, + CleanupFailed { + provider: String, +- error: String, ++ error: String, + }, + + // -- Docker -- +@@ -217,7 +217,7 @@ pub enum SandboxEvent { + name: String, + }, + SnapshotPulled { +- name: String, ++ name: String, + duration_ms: u64, + }, + +@@ -229,25 +229,25 @@ pub enum SandboxEvent { + name: String, + }, + SnapshotReady { +- name: String, ++ name: String, + duration_ms: u64, + }, + SnapshotFailed { +- name: String, ++ name: String, + error: String, + }, + + // -- Daytona git -- + GitCloneStarted { +- url: String, ++ url: String, + branch: Option, + }, + GitCloneCompleted { +- url: String, ++ url: String, + duration_ms: u64, + }, + GitCloneFailed { +- url: String, ++ url: String, + error: String, + }, + } +@@ -344,25 +344,25 @@ pub fn format_lines_numbered(content: &str, offset: Option, limit: Option + + #[derive(Debug, Clone)] + pub struct ExecResult { +- pub stdout: String, +- pub stderr: String, +- pub exit_code: i32, +- pub timed_out: bool, ++ pub stdout: String, ++ pub stderr: String, ++ pub exit_code: i32, ++ pub timed_out: bool, + pub duration_ms: u64, + } + + #[derive(Debug, Clone)] + pub struct DirEntry { +- pub name: String, ++ pub name: String, + pub is_dir: bool, +- pub size: Option, ++ pub size: Option, + } + + #[derive(Debug, Clone, Default)] + pub struct GrepOptions { +- pub glob_filter: Option, ++ pub glob_filter: Option, + pub case_insensitive: bool, +- pub max_results: Option, ++ pub max_results: Option, + } + + #[async_trait] +@@ -612,10 +612,10 @@ mod tests { + #[test] + fn exec_result_fields() { + let result = ExecResult { +- stdout: "out".into(), +- stderr: "err".into(), +- exit_code: 1, +- timed_out: true, ++ stdout: "out".into(), ++ stderr: "err".into(), ++ exit_code: 1, ++ timed_out: true, + duration_ms: 5000, + }; + assert_eq!(result.exit_code, 1); +@@ -626,9 +626,9 @@ mod tests { + #[test] + fn dir_entry_fields() { + let entry = DirEntry { +- name: "src".into(), ++ name: "src".into(), + is_dir: true, +- size: None, ++ size: None, + }; + assert_eq!(entry.name, "src"); + assert!(entry.is_dir); +@@ -650,34 +650,34 @@ mod tests { + provider: "local".into(), + }, + SandboxEvent::Ready { +- provider: "local".into(), ++ provider: "local".into(), + duration_ms: 50, +- name: None, +- cpu: None, +- memory: None, +- url: None, ++ name: None, ++ cpu: None, ++ memory: None, ++ url: None, + }, + SandboxEvent::InitializeFailed { +- provider: "docker".into(), +- error: "no daemon".into(), ++ provider: "docker".into(), ++ error: "no daemon".into(), + duration_ms: 100, + }, + SandboxEvent::CleanupStarted { + provider: "daytona".into(), + }, + SandboxEvent::CleanupCompleted { +- provider: "daytona".into(), ++ provider: "daytona".into(), + duration_ms: 200, + }, + SandboxEvent::CleanupFailed { + provider: "docker".into(), +- error: "container gone".into(), ++ error: "container gone".into(), + }, + SandboxEvent::SnapshotPulling { + name: "ubuntu:22.04".into(), + }, + SandboxEvent::SnapshotPulled { +- name: "ubuntu:22.04".into(), ++ name: "ubuntu:22.04".into(), + duration_ms: 5000, + }, + SandboxEvent::SnapshotEnsuring { +@@ -687,23 +687,23 @@ mod tests { + name: "my-snap".into(), + }, + SandboxEvent::SnapshotReady { +- name: "my-snap".into(), ++ name: "my-snap".into(), + duration_ms: 30000, + }, + SandboxEvent::SnapshotFailed { +- name: "my-snap".into(), ++ name: "my-snap".into(), + error: "build failed".into(), + }, + SandboxEvent::GitCloneStarted { +- url: "https://github.com/org/repo.git".into(), ++ url: "https://github.com/org/repo.git".into(), + branch: Some("main".into()), + }, + SandboxEvent::GitCloneCompleted { +- url: "https://github.com/org/repo.git".into(), ++ url: "https://github.com/org/repo.git".into(), + duration_ms: 8000, + }, + SandboxEvent::GitCloneFailed { +- url: "https://github.com/org/repo.git".into(), ++ url: "https://github.com/org/repo.git".into(), + error: "auth failed".into(), + }, + ]; +diff --git a/lib/crates/fabro-sandbox/src/sandbox_spec.rs b/lib/crates/fabro-sandbox/src/sandbox_spec.rs +index e3633bda..19c5384a 100644 +--- a/lib/crates/fabro-sandbox/src/sandbox_spec.rs ++++ b/lib/crates/fabro-sandbox/src/sandbox_spec.rs +@@ -27,11 +27,11 @@ pub enum SandboxSpec { + }, + #[cfg(feature = "daytona")] + Daytona { +- config: DaytonaConfig, +- github_app: Option, +- run_id: Option, ++ config: DaytonaConfig, ++ github_app: Option, ++ run_id: Option, + clone_branch: Option, +- api_key: Option, ++ api_key: Option, + }, + } + +@@ -157,15 +157,15 @@ impl SandboxSpec { + #[cfg(feature = "docker")] + Self::Docker { config } => { + let mut sandbox = DockerSandbox::new(DockerSandboxOptions { +- image: config.image.clone(), ++ image: config.image.clone(), + host_working_directory: config.host_working_directory.clone(), +- container_mount_point: config.container_mount_point.clone(), +- network_mode: config.network_mode.clone(), +- extra_mounts: config.extra_mounts.clone(), +- memory_limit: config.memory_limit, +- cpu_quota: config.cpu_quota, +- auto_pull: config.auto_pull, +- env_vars: config.env_vars.clone(), ++ container_mount_point: config.container_mount_point.clone(), ++ network_mode: config.network_mode.clone(), ++ extra_mounts: config.extra_mounts.clone(), ++ memory_limit: config.memory_limit, ++ cpu_quota: config.cpu_quota, ++ auto_pull: config.auto_pull, ++ env_vars: config.env_vars.clone(), + }) + .map_err(|e| anyhow!("Failed to create Docker sandbox: {e}"))?; + if let Some(callback) = event_callback { +diff --git a/lib/crates/fabro-sandbox/src/test_support.rs b/lib/crates/fabro-sandbox/src/test_support.rs +index d9fe1423..a166daaf 100644 +--- a/lib/crates/fabro-sandbox/src/test_support.rs ++++ b/lib/crates/fabro-sandbox/src/test_support.rs +@@ -10,28 +10,28 @@ use crate::{DirEntry, ExecResult, GrepOptions, Sandbox, SandboxEvent, SandboxEve + // --- MockSandbox --- + + pub struct MockSandbox { +- pub files: HashMap, +- pub exec_result: ExecResult, +- pub grep_results: Vec, +- pub glob_results: Vec, +- pub working_dir: &'static str, +- pub platform_str: &'static str, +- pub os_version_str: String, ++ pub files: HashMap, ++ pub exec_result: ExecResult, ++ pub grep_results: Vec, ++ pub glob_results: Vec, ++ pub working_dir: &'static str, ++ pub platform_str: &'static str, ++ pub os_version_str: String, + /// When true, `read_file` applies offset/limit by splitting on lines. + pub apply_read_offset_limit: bool, + /// Captures (path, content) pairs from `write_file` calls. +- pub written_files: Mutex>, ++ pub written_files: Mutex>, + /// Captures the `timeout_ms` argument from `exec_command` calls. +- pub captured_timeout: Mutex>, ++ pub captured_timeout: Mutex>, + /// Captures the `command` argument from `exec_command` calls (last only). +- pub captured_command: Mutex>, ++ pub captured_command: Mutex>, + /// Captures all `command` arguments from `exec_command` calls in order. +- pub captured_commands: Mutex>, ++ pub captured_commands: Mutex>, + /// Captures all `working_dir` arguments from `exec_command` calls in order. +- pub captured_working_dirs: Mutex>>, ++ pub captured_working_dirs: Mutex>>, + /// Captures the `env_vars` argument from `exec_command` calls. +- pub captured_env_vars: Mutex>>, +- pub event_callback: Option, ++ pub captured_env_vars: Mutex>>, ++ pub event_callback: Option, + } + + impl MockSandbox { +@@ -57,27 +57,27 @@ impl MockSandbox { + impl Default for MockSandbox { + fn default() -> Self { + Self { +- files: HashMap::new(), +- exec_result: ExecResult { +- stdout: "mock output".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ files: HashMap::new(), ++ exec_result: ExecResult { ++ stdout: "mock output".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 10, + }, +- grep_results: vec![], +- glob_results: vec![], +- working_dir: "/work", +- platform_str: "darwin", +- os_version_str: "Darwin 24.0.0".into(), ++ grep_results: vec![], ++ glob_results: vec![], ++ working_dir: "/work", ++ platform_str: "darwin", ++ os_version_str: "Darwin 24.0.0".into(), + apply_read_offset_limit: false, +- written_files: Mutex::new(Vec::new()), +- captured_timeout: Mutex::new(None), +- captured_command: Mutex::new(None), +- captured_commands: Mutex::new(Vec::new()), +- captured_working_dirs: Mutex::new(Vec::new()), +- captured_env_vars: Mutex::new(None), +- event_callback: None, ++ written_files: Mutex::new(Vec::new()), ++ captured_timeout: Mutex::new(None), ++ captured_command: Mutex::new(None), ++ captured_commands: Mutex::new(Vec::new()), ++ captured_working_dirs: Mutex::new(Vec::new()), ++ captured_env_vars: Mutex::new(None), ++ event_callback: None, + } + } + } +@@ -211,12 +211,12 @@ impl Sandbox for MockSandbox { + provider: "mock".into(), + }); + self.emit(SandboxEvent::Ready { +- provider: "mock".into(), ++ provider: "mock".into(), + duration_ms: 0, +- name: None, +- cpu: None, +- memory: None, +- url: None, ++ name: None, ++ cpu: None, ++ memory: None, ++ url: None, + }); + Ok(()) + } +@@ -226,7 +226,7 @@ impl Sandbox for MockSandbox { + provider: "mock".into(), + }); + self.emit(SandboxEvent::CleanupCompleted { +- provider: "mock".into(), ++ provider: "mock".into(), + duration_ms: 0, + }); + Ok(()) +@@ -316,10 +316,10 @@ impl Sandbox for MutableMockSandbox { + _cancel_token: Option, + ) -> Result { + Ok(ExecResult { +- stdout: String::new(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: String::new(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 0, + }) + } +diff --git a/lib/crates/fabro-sandbox/src/worktree.rs b/lib/crates/fabro-sandbox/src/worktree.rs +index da4d3c3e..75e09b86 100644 +--- a/lib/crates/fabro-sandbox/src/worktree.rs ++++ b/lib/crates/fabro-sandbox/src/worktree.rs +@@ -26,9 +26,9 @@ pub type WorktreeEventCallback = Arc; + + /// Configuration for a `WorktreeSandbox`. + pub struct WorktreeOptions { +- pub branch_name: String, +- pub base_sha: String, +- pub worktree_path: String, ++ pub branch_name: String, ++ pub base_sha: String, ++ pub worktree_path: String, + /// Skip branch creation and hard reset (for resume, where branch already + /// exists). + pub skip_branch_creation: bool, +@@ -41,10 +41,10 @@ pub struct WorktreeOptions { + /// `initialize()` and `cleanup()` do NOT call the inner sandbox's lifecycle + /// methods. The inner sandbox's lifecycle is managed separately by the caller. + pub struct WorktreeSandbox { +- inner: Arc, +- config: WorktreeOptions, ++ inner: Arc, ++ config: WorktreeOptions, + event_callback: Option, +- initialized: std::sync::atomic::AtomicBool, ++ initialized: std::sync::atomic::AtomicBool, + } + + impl WorktreeSandbox { +@@ -153,7 +153,7 @@ impl Sandbox for WorktreeSandbox { + } + self.emit(WorktreeEvent::BranchCreated { + branch: self.config.branch_name.clone(), +- sha: self.config.base_sha.clone(), ++ sha: self.config.base_sha.clone(), + }); + } + +@@ -178,7 +178,7 @@ impl Sandbox for WorktreeSandbox { + )); + } + self.emit(WorktreeEvent::WorktreeAdded { +- path: self.config.worktree_path.clone(), ++ path: self.config.worktree_path.clone(), + branch: self.config.branch_name.clone(), + }); + +@@ -367,18 +367,18 @@ mod tests { + + fn make_config(wt_path: &str) -> WorktreeOptions { + WorktreeOptions { +- branch_name: "fabro/run/test-branch".to_string(), +- base_sha: "abc123def456".to_string(), +- worktree_path: wt_path.to_string(), ++ branch_name: "fabro/run/test-branch".to_string(), ++ base_sha: "abc123def456".to_string(), ++ worktree_path: wt_path.to_string(), + skip_branch_creation: false, + } + } + + fn make_config_skip(wt_path: &str) -> WorktreeOptions { + WorktreeOptions { +- branch_name: "fabro/run/test-branch".to_string(), +- base_sha: "abc123def456".to_string(), +- worktree_path: wt_path.to_string(), ++ branch_name: "fabro/run/test-branch".to_string(), ++ base_sha: "abc123def456".to_string(), ++ worktree_path: wt_path.to_string(), + skip_branch_creation: true, + } + } +@@ -442,9 +442,9 @@ mod tests { + async fn initialize_uses_shell_quoted_values_in_commands() { + let (inner, mock) = make_mock(); + let config = WorktreeOptions { +- branch_name: "fabro/run/my-branch".to_string(), +- base_sha: "deadbeef".to_string(), +- worktree_path: "/tmp/my worktree".to_string(), // path with space ++ branch_name: "fabro/run/my-branch".to_string(), ++ base_sha: "deadbeef".to_string(), ++ worktree_path: "/tmp/my worktree".to_string(), // path with space + skip_branch_creation: false, + }; + let wt = WorktreeSandbox::new(inner, config); +@@ -514,10 +514,10 @@ mod tests { + async fn initialize_propagates_error_on_nonzero_exit() { + let inner: Arc = Arc::new(MockSandbox { + exec_result: ExecResult { +- stdout: String::new(), +- stderr: "fatal: not a git repo".to_string(), +- exit_code: 128, +- timed_out: false, ++ stdout: String::new(), ++ stderr: "fatal: not a git repo".to_string(), ++ exit_code: 128, ++ timed_out: false, + duration_ms: 5, + }, + ..MockSandbox::linux() +@@ -662,9 +662,9 @@ mod tests { + + let inner: Arc = Arc::new(LocalSandbox::new(original.clone())); + let config = WorktreeOptions { +- branch_name: "test-branch".into(), +- base_sha: "abc123".into(), +- worktree_path: worktree.to_string_lossy().to_string(), ++ branch_name: "test-branch".into(), ++ base_sha: "abc123".into(), ++ worktree_path: worktree.to_string_lossy().to_string(), + skip_branch_creation: false, + }; + let wt = WorktreeSandbox::new(inner, config); +@@ -703,9 +703,9 @@ mod tests { + + let inner: Arc = Arc::new(LocalSandbox::new(original.clone())); + let config = WorktreeOptions { +- branch_name: "test-branch".into(), +- base_sha: "abc123".into(), +- worktree_path: worktree.to_string_lossy().to_string(), ++ branch_name: "test-branch".into(), ++ base_sha: "abc123".into(), ++ worktree_path: worktree.to_string_lossy().to_string(), + skip_branch_creation: false, + }; + let wt = WorktreeSandbox::new(inner, config); +@@ -737,9 +737,9 @@ mod tests { + + let inner: Arc = Arc::new(LocalSandbox::new(original.clone())); + let config = WorktreeOptions { +- branch_name: "test-branch".into(), +- base_sha: "abc123".into(), +- worktree_path: worktree.to_string_lossy().to_string(), ++ branch_name: "test-branch".into(), ++ base_sha: "abc123".into(), ++ worktree_path: worktree.to_string_lossy().to_string(), + skip_branch_creation: false, + }; + let wt = WorktreeSandbox::new(inner, config); +@@ -763,9 +763,9 @@ mod tests { + fn accessors_return_config_values() { + let (inner, _mock) = make_mock(); + let config = WorktreeOptions { +- branch_name: "my-branch".to_string(), +- base_sha: "sha123".to_string(), +- worktree_path: "/path/to/wt".to_string(), ++ branch_name: "my-branch".to_string(), ++ base_sha: "sha123".to_string(), ++ worktree_path: "/path/to/wt".to_string(), + skip_branch_creation: false, + }; + let wt = WorktreeSandbox::new(inner, config); +diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs +index b7e18879..86950218 100644 +--- a/lib/crates/fabro-server/src/demo/mod.rs ++++ b/lib/crates/fabro-server/src/demo/mod.rs +@@ -675,454 +675,445 @@ mod runs { + pub(super) fn list_items() -> Vec { + vec![ + RunListItem { +- id: "run-1".into(), +- repository: RepositoryReference { ++ id: "run-1".into(), ++ repository: RepositoryReference { + name: "api-server".into(), + }, +- title: "Add rate limiting to auth endpoints".into(), +- workflow: WorkflowReference { ++ title: "Add rate limiting to auth endpoints".into(), ++ workflow: WorkflowReference { + slug: "implement".into(), + }, +- status: BoardColumn::Working, ++ status: BoardColumn::Working, + pull_request: None, +- timings: Some(RunTimings { +- elapsed_secs: 420.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 420.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-a1b2c3d4".into(), +- resources: Some(SandboxResources { +- cpu: 4, +- memory: 8, +- }), ++ sandbox: Some(RunSandbox { ++ id: "sb-a1b2c3d4".into(), ++ resources: Some(SandboxResources { cpu: 4, memory: 8 }), + }), +- question: None, +- created_at: ts("2026-03-06T14:30:00Z"), ++ question: None, ++ created_at: ts("2026-03-06T14:30:00Z"), + }, + RunListItem { +- id: "run-2".into(), +- repository: RepositoryReference { ++ id: "run-2".into(), ++ repository: RepositoryReference { + name: "web-dashboard".into(), + }, +- title: "Migrate to React Router v7".into(), +- workflow: WorkflowReference { ++ title: "Migrate to React Router v7".into(), ++ workflow: WorkflowReference { + slug: "implement".into(), + }, +- status: BoardColumn::Working, ++ status: BoardColumn::Working, + pull_request: None, +- timings: Some(RunTimings { +- elapsed_secs: 8100.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 8100.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-e5f6g7h8".into(), +- resources: Some(SandboxResources { +- cpu: 8, +- memory: 16, +- }), ++ sandbox: Some(RunSandbox { ++ id: "sb-e5f6g7h8".into(), ++ resources: Some(SandboxResources { cpu: 8, memory: 16 }), + }), +- question: None, +- created_at: ts("2026-03-06T12:00:00Z"), ++ question: None, ++ created_at: ts("2026-03-06T12:00:00Z"), + }, + RunListItem { +- id: "run-3".into(), +- repository: RepositoryReference { ++ id: "run-3".into(), ++ repository: RepositoryReference { + name: "cli-tools".into(), + }, +- title: "Fix config parsing for nested values".into(), +- workflow: WorkflowReference { ++ title: "Fix config parsing for nested values".into(), ++ workflow: WorkflowReference { + slug: "fix_build".into(), + }, +- status: BoardColumn::Working, ++ status: BoardColumn::Working, + pull_request: None, +- timings: Some(RunTimings { +- elapsed_secs: 2700.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 2700.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-i9j0k1l2".into(), +- resources: Some(SandboxResources { +- cpu: 2, +- memory: 4, +- }), ++ sandbox: Some(RunSandbox { ++ id: "sb-i9j0k1l2".into(), ++ resources: Some(SandboxResources { cpu: 2, memory: 4 }), + }), +- question: None, +- created_at: ts("2026-03-05T09:20:00Z"), ++ question: None, ++ created_at: ts("2026-03-05T09:20:00Z"), + }, + RunListItem { +- id: "run-4".into(), +- repository: RepositoryReference { ++ id: "run-4".into(), ++ repository: RepositoryReference { + name: "api-server".into(), + }, +- title: "Update OpenAPI spec for v3".into(), +- workflow: WorkflowReference { ++ title: "Update OpenAPI spec for v3".into(), ++ workflow: WorkflowReference { + slug: "expand".into(), + }, +- status: BoardColumn::Initializing, ++ status: BoardColumn::Initializing, + pull_request: Some(RunPullRequest { +- number: 0, ++ number: 0, + additions: Some(567), + deletions: Some(234), +- comments: Some(0), +- checks: vec![], ++ comments: Some(0), ++ checks: vec![], + }), +- timings: Some(RunTimings { +- elapsed_secs: 4320.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 4320.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-q7r8s9t0".into(), ++ sandbox: Some(RunSandbox { ++ id: "sb-q7r8s9t0".into(), + resources: None, + }), +- question: Some(RunQuestion { ++ question: Some(RunQuestion { + text: "Accept or push for another round?".into(), + }), +- created_at: ts("2026-03-04T15:00:00Z"), ++ created_at: ts("2026-03-04T15:00:00Z"), + }, + RunListItem { +- id: "run-5".into(), +- repository: RepositoryReference { ++ id: "run-5".into(), ++ repository: RepositoryReference { + name: "shared-types".into(), + }, +- title: "Add pipeline event types".into(), +- workflow: WorkflowReference { ++ title: "Add pipeline event types".into(), ++ workflow: WorkflowReference { + slug: "implement".into(), + }, +- status: BoardColumn::Initializing, ++ status: BoardColumn::Initializing, + pull_request: Some(RunPullRequest { +- number: 0, ++ number: 0, + additions: Some(145), + deletions: Some(23), +- comments: Some(0), +- checks: vec![], ++ comments: Some(0), ++ checks: vec![], + }), +- timings: Some(RunTimings { +- elapsed_secs: 1680.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 1680.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-u1v2w3x4".into(), ++ sandbox: Some(RunSandbox { ++ id: "sb-u1v2w3x4".into(), + resources: None, + }), +- question: Some(RunQuestion { ++ question: Some(RunQuestion { + text: "Proceed from investigation to fix?".into(), + }), +- created_at: ts("2026-03-04T10:00:00Z"), ++ created_at: ts("2026-03-04T10:00:00Z"), + }, + RunListItem { +- id: "run-6".into(), +- repository: RepositoryReference { ++ id: "run-6".into(), ++ repository: RepositoryReference { + name: "web-dashboard".into(), + }, +- title: "Add dark mode toggle".into(), +- workflow: WorkflowReference { ++ title: "Add dark mode toggle".into(), ++ workflow: WorkflowReference { + slug: "implement".into(), + }, +- status: BoardColumn::Review, ++ status: BoardColumn::Review, + pull_request: Some(RunPullRequest { +- number: 889, ++ number: 889, + additions: Some(234), + deletions: Some(67), +- comments: Some(4), +- checks: vec![ ++ comments: Some(4), ++ checks: vec![ + CheckRun { +- name: "lint".into(), +- status: CheckRunStatus::Success, ++ name: "lint".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(23.0), + }, + CheckRun { +- name: "typecheck".into(), +- status: CheckRunStatus::Success, ++ name: "typecheck".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(72.0), + }, + CheckRun { +- name: "unit-tests".into(), +- status: CheckRunStatus::Success, ++ name: "unit-tests".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(154.0), + }, + CheckRun { +- name: "integration-tests".into(), +- status: CheckRunStatus::Failure, ++ name: "integration-tests".into(), ++ status: CheckRunStatus::Failure, + duration_secs: Some(296.0), + }, + CheckRun { +- name: "e2e / chrome".into(), +- status: CheckRunStatus::Failure, ++ name: "e2e / chrome".into(), ++ status: CheckRunStatus::Failure, + duration_secs: Some(182.0), + }, + CheckRun { +- name: "build".into(), +- status: CheckRunStatus::Success, ++ name: "build".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(105.0), + }, + CheckRun { +- name: "coverage".into(), +- status: CheckRunStatus::Skipped, ++ name: "coverage".into(), ++ status: CheckRunStatus::Skipped, + duration_secs: None, + }, + ], + }), +- timings: Some(RunTimings { +- elapsed_secs: 2100.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 2100.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-m3n4o5p6".into(), ++ sandbox: Some(RunSandbox { ++ id: "sb-m3n4o5p6".into(), + resources: None, + }), +- question: None, +- created_at: ts("2026-03-03T16:45:00Z"), ++ question: None, ++ created_at: ts("2026-03-03T16:45:00Z"), + }, + RunListItem { +- id: "run-7".into(), +- repository: RepositoryReference { ++ id: "run-7".into(), ++ repository: RepositoryReference { + name: "infrastructure".into(), + }, +- title: "Terraform module for Redis cluster".into(), +- workflow: WorkflowReference { ++ title: "Terraform module for Redis cluster".into(), ++ workflow: WorkflowReference { + slug: "implement".into(), + }, +- status: BoardColumn::Review, ++ status: BoardColumn::Review, + pull_request: Some(RunPullRequest { +- number: 156, ++ number: 156, + additions: Some(412), + deletions: Some(0), +- comments: Some(1), +- checks: vec![ ++ comments: Some(1), ++ checks: vec![ + CheckRun { +- name: "lint".into(), +- status: CheckRunStatus::Success, ++ name: "lint".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(18.0), + }, + CheckRun { +- name: "typecheck".into(), +- status: CheckRunStatus::Success, ++ name: "typecheck".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(56.0), + }, + CheckRun { +- name: "unit-tests".into(), +- status: CheckRunStatus::Pending, ++ name: "unit-tests".into(), ++ status: CheckRunStatus::Pending, + duration_secs: None, + }, + CheckRun { +- name: "integration-tests".into(), +- status: CheckRunStatus::Queued, ++ name: "integration-tests".into(), ++ status: CheckRunStatus::Queued, + duration_secs: None, + }, + CheckRun { +- name: "build".into(), +- status: CheckRunStatus::Pending, ++ name: "build".into(), ++ status: CheckRunStatus::Pending, + duration_secs: None, + }, + ], + }), +- timings: Some(RunTimings { +- elapsed_secs: 720.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 720.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-y5z6a7b8".into(), ++ sandbox: Some(RunSandbox { ++ id: "sb-y5z6a7b8".into(), + resources: None, + }), +- question: None, +- created_at: ts("2026-03-03T11:00:00Z"), ++ question: None, ++ created_at: ts("2026-03-03T11:00:00Z"), + }, + RunListItem { +- id: "run-8".into(), +- repository: RepositoryReference { ++ id: "run-8".into(), ++ repository: RepositoryReference { + name: "api-server".into(), + }, +- title: "Implement webhook retry logic".into(), +- workflow: WorkflowReference { ++ title: "Implement webhook retry logic".into(), ++ workflow: WorkflowReference { + slug: "implement".into(), + }, +- status: BoardColumn::Merge, ++ status: BoardColumn::Merge, + pull_request: Some(RunPullRequest { +- number: 1249, ++ number: 1249, + additions: Some(189), + deletions: Some(45), +- comments: Some(7), +- checks: vec![ ++ comments: Some(7), ++ checks: vec![ + CheckRun { +- name: "lint".into(), +- status: CheckRunStatus::Success, ++ name: "lint".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(21.0), + }, + CheckRun { +- name: "typecheck".into(), +- status: CheckRunStatus::Success, ++ name: "typecheck".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(68.0), + }, + CheckRun { +- name: "unit-tests".into(), +- status: CheckRunStatus::Success, ++ name: "unit-tests".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(192.0), + }, + CheckRun { +- name: "integration-tests".into(), +- status: CheckRunStatus::Success, ++ name: "integration-tests".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(334.0), + }, + CheckRun { +- name: "e2e / chrome".into(), +- status: CheckRunStatus::Success, ++ name: "e2e / chrome".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(262.0), + }, + CheckRun { +- name: "e2e / firefox".into(), +- status: CheckRunStatus::Success, ++ name: "e2e / firefox".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(285.0), + }, + CheckRun { +- name: "build".into(), +- status: CheckRunStatus::Success, ++ name: "build".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(121.0), + }, + CheckRun { +- name: "deploy-preview".into(), +- status: CheckRunStatus::Success, ++ name: "deploy-preview".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(93.0), + }, + CheckRun { +- name: "security-scan".into(), +- status: CheckRunStatus::Skipped, ++ name: "security-scan".into(), ++ status: CheckRunStatus::Skipped, + duration_secs: None, + }, + CheckRun { +- name: "performance".into(), +- status: CheckRunStatus::Success, ++ name: "performance".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(138.0), + }, + CheckRun { +- name: "bundle-size".into(), +- status: CheckRunStatus::Success, ++ name: "bundle-size".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(34.0), + }, + CheckRun { +- name: "accessibility".into(), +- status: CheckRunStatus::Success, ++ name: "accessibility".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(72.0), + }, + ], + }), +- timings: Some(RunTimings { +- elapsed_secs: 259200.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 259200.0, + elapsed_warning: Some(true), + }), +- sandbox: Some(RunSandbox { +- id: "sb-c9d0e1f2".into(), ++ sandbox: Some(RunSandbox { ++ id: "sb-c9d0e1f2".into(), + resources: None, + }), +- question: None, +- created_at: ts("2026-02-28T14:00:00Z"), ++ question: None, ++ created_at: ts("2026-02-28T14:00:00Z"), + }, + RunListItem { +- id: "run-9".into(), +- repository: RepositoryReference { ++ id: "run-9".into(), ++ repository: RepositoryReference { + name: "cli-tools".into(), + }, +- title: "Add --verbose flag to run command".into(), +- workflow: WorkflowReference { ++ title: "Add --verbose flag to run command".into(), ++ workflow: WorkflowReference { + slug: "expand".into(), + }, +- status: BoardColumn::Merge, ++ status: BoardColumn::Merge, + pull_request: Some(RunPullRequest { +- number: 430, ++ number: 430, + additions: Some(56), + deletions: Some(12), +- comments: Some(2), +- checks: vec![ ++ comments: Some(2), ++ checks: vec![ + CheckRun { +- name: "lint".into(), +- status: CheckRunStatus::Success, ++ name: "lint".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(15.0), + }, + CheckRun { +- name: "typecheck".into(), +- status: CheckRunStatus::Success, ++ name: "typecheck".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(48.0), + }, + CheckRun { +- name: "unit-tests".into(), +- status: CheckRunStatus::Success, ++ name: "unit-tests".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(116.0), + }, + CheckRun { +- name: "build".into(), +- status: CheckRunStatus::Success, ++ name: "build".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(82.0), + }, + CheckRun { +- name: "coverage".into(), +- status: CheckRunStatus::Success, ++ name: "coverage".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(124.0), + }, + CheckRun { +- name: "bundle-size".into(), +- status: CheckRunStatus::Skipped, ++ name: "bundle-size".into(), ++ status: CheckRunStatus::Skipped, + duration_secs: None, + }, + ], + }), +- timings: Some(RunTimings { +- elapsed_secs: 3900.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 3900.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-g3h4i5j6".into(), ++ sandbox: Some(RunSandbox { ++ id: "sb-g3h4i5j6".into(), + resources: None, + }), +- question: None, +- created_at: ts("2026-02-27T09:00:00Z"), ++ question: None, ++ created_at: ts("2026-02-27T09:00:00Z"), + }, + RunListItem { +- id: "run-10".into(), +- repository: RepositoryReference { ++ id: "run-10".into(), ++ repository: RepositoryReference { + name: "shared-types".into(), + }, +- title: "Export utility type helpers".into(), +- workflow: WorkflowReference { ++ title: "Export utility type helpers".into(), ++ workflow: WorkflowReference { + slug: "sync_drift".into(), + }, +- status: BoardColumn::Merge, ++ status: BoardColumn::Merge, + pull_request: Some(RunPullRequest { +- number: 76, ++ number: 76, + additions: Some(34), + deletions: Some(8), +- comments: Some(0), +- checks: vec![ ++ comments: Some(0), ++ checks: vec![ + CheckRun { +- name: "lint".into(), +- status: CheckRunStatus::Success, ++ name: "lint".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(12.0), + }, + CheckRun { +- name: "typecheck".into(), +- status: CheckRunStatus::Success, ++ name: "typecheck".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(34.0), + }, + CheckRun { +- name: "unit-tests".into(), +- status: CheckRunStatus::Success, ++ name: "unit-tests".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(75.0), + }, + CheckRun { +- name: "build".into(), +- status: CheckRunStatus::Success, ++ name: "build".into(), ++ status: CheckRunStatus::Success, + duration_secs: Some(58.0), + }, + ], + }), +- timings: Some(RunTimings { +- elapsed_secs: 2880.0, ++ timings: Some(RunTimings { ++ elapsed_secs: 2880.0, + elapsed_warning: Some(false), + }), +- sandbox: Some(RunSandbox { +- id: "sb-k7l8m9n0".into(), ++ sandbox: Some(RunSandbox { ++ id: "sb-k7l8m9n0".into(), + resources: None, + }), +- question: None, +- created_at: ts("2026-02-26T08:00:00Z"), ++ question: None, ++ created_at: ts("2026-02-26T08:00:00Z"), + }, + ] + } +@@ -1130,32 +1121,32 @@ mod runs { + pub(super) fn stages() -> Vec { + vec![ + RunStage { +- id: "detect-drift".into(), +- name: "Detect Drift".into(), +- status: StageStatus::Completed, ++ id: "detect-drift".into(), ++ name: "Detect Drift".into(), ++ status: StageStatus::Completed, + duration_secs: Some(72.0), +- dot_id: Some("detect".into()), ++ dot_id: Some("detect".into()), + }, + RunStage { +- id: "propose-changes".into(), +- name: "Propose Changes".into(), +- status: StageStatus::Completed, ++ id: "propose-changes".into(), ++ name: "Propose Changes".into(), ++ status: StageStatus::Completed, + duration_secs: Some(154.0), +- dot_id: Some("propose".into()), ++ dot_id: Some("propose".into()), + }, + RunStage { +- id: "review-changes".into(), +- name: "Review Changes".into(), +- status: StageStatus::Completed, ++ id: "review-changes".into(), ++ name: "Review Changes".into(), ++ status: StageStatus::Completed, + duration_secs: Some(45.0), +- dot_id: Some("review".into()), ++ dot_id: Some("review".into()), + }, + RunStage { +- id: "apply-changes".into(), +- name: "Apply Changes".into(), +- status: StageStatus::Running, ++ id: "apply-changes".into(), ++ name: "Apply Changes".into(), ++ status: StageStatus::Running, + duration_secs: Some(118.0), +- dot_id: Some("apply".into()), ++ dot_id: Some("apply".into()), + }, + ] + } +@@ -1177,139 +1168,139 @@ mod runs { + + pub(super) fn billing() -> RunBilling { + RunBilling { +- stages: vec![ ++ stages: vec![ + RunBillingStage { +- stage: BillingStageRef { +- id: "detect-drift".into(), ++ stage: BillingStageRef { ++ id: "detect-drift".into(), + name: "Detect Drift".into(), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Opus 4.6".into(), + }, +- billing: BilledTokenCounts { +- cache_read_tokens: None, ++ billing: BilledTokenCounts { ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 12480, +- output_tokens: 3210, +- reasoning_tokens: None, +- total_tokens: 15690, +- total_usd_micros: Some(480_000), ++ input_tokens: 12480, ++ output_tokens: 3210, ++ reasoning_tokens: None, ++ total_tokens: 15690, ++ total_usd_micros: Some(480_000), + }, + runtime_secs: 72.0, + }, + RunBillingStage { +- stage: BillingStageRef { +- id: "propose-changes".into(), ++ stage: BillingStageRef { ++ id: "propose-changes".into(), + name: "Propose Changes".into(), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Gemini 3.1".into(), + }, +- billing: BilledTokenCounts { +- cache_read_tokens: None, ++ billing: BilledTokenCounts { ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 28640, +- output_tokens: 8750, +- reasoning_tokens: None, +- total_tokens: 37390, +- total_usd_micros: Some(720_000), ++ input_tokens: 28640, ++ output_tokens: 8750, ++ reasoning_tokens: None, ++ total_tokens: 37390, ++ total_usd_micros: Some(720_000), + }, + runtime_secs: 154.0, + }, + RunBillingStage { +- stage: BillingStageRef { +- id: "review-changes".into(), ++ stage: BillingStageRef { ++ id: "review-changes".into(), + name: "Review Changes".into(), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Codex 5.3".into(), + }, +- billing: BilledTokenCounts { +- cache_read_tokens: None, ++ billing: BilledTokenCounts { ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 9120, +- output_tokens: 2640, +- reasoning_tokens: None, +- total_tokens: 11760, +- total_usd_micros: Some(190_000), ++ input_tokens: 9120, ++ output_tokens: 2640, ++ reasoning_tokens: None, ++ total_tokens: 11760, ++ total_usd_micros: Some(190_000), + }, + runtime_secs: 45.0, + }, + RunBillingStage { +- stage: BillingStageRef { +- id: "apply-changes".into(), ++ stage: BillingStageRef { ++ id: "apply-changes".into(), + name: "Apply Changes".into(), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Opus 4.6".into(), + }, +- billing: BilledTokenCounts { +- cache_read_tokens: None, ++ billing: BilledTokenCounts { ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 21300, +- output_tokens: 6480, +- reasoning_tokens: None, +- total_tokens: 27780, +- total_usd_micros: Some(870_000), ++ input_tokens: 21300, ++ output_tokens: 6480, ++ reasoning_tokens: None, ++ total_tokens: 27780, ++ total_usd_micros: Some(870_000), + }, + runtime_secs: 118.0, + }, + ], +- totals: RunBillingTotals { +- cache_read_tokens: None, ++ totals: RunBillingTotals { ++ cache_read_tokens: None, + cache_write_tokens: None, +- runtime_secs: 389.0, +- input_tokens: 71540, +- output_tokens: 21080, +- reasoning_tokens: None, +- total_tokens: 92620, +- total_usd_micros: Some(2_260_000), ++ runtime_secs: 389.0, ++ input_tokens: 71540, ++ output_tokens: 21080, ++ reasoning_tokens: None, ++ total_tokens: 92620, ++ total_usd_micros: Some(2_260_000), + }, + by_model: vec![ + BillingByModel { + billing: BilledTokenCounts { +- cache_read_tokens: None, ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 33780, +- output_tokens: 9690, +- reasoning_tokens: None, +- total_tokens: 43470, +- total_usd_micros: Some(1_350_000), ++ input_tokens: 33780, ++ output_tokens: 9690, ++ reasoning_tokens: None, ++ total_tokens: 43470, ++ total_usd_micros: Some(1_350_000), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Opus 4.6".into(), + }, +- stages: 2, ++ stages: 2, + }, + BillingByModel { + billing: BilledTokenCounts { +- cache_read_tokens: None, ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 28640, +- output_tokens: 8750, +- reasoning_tokens: None, +- total_tokens: 37390, +- total_usd_micros: Some(720_000), ++ input_tokens: 28640, ++ output_tokens: 8750, ++ reasoning_tokens: None, ++ total_tokens: 37390, ++ total_usd_micros: Some(720_000), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Gemini 3.1".into(), + }, +- stages: 1, ++ stages: 1, + }, + BillingByModel { + billing: BilledTokenCounts { +- cache_read_tokens: None, ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 9120, +- output_tokens: 2640, +- reasoning_tokens: None, +- total_tokens: 11760, +- total_usd_micros: Some(190_000), ++ input_tokens: 9120, ++ output_tokens: 2640, ++ reasoning_tokens: None, ++ total_tokens: 11760, ++ total_usd_micros: Some(190_000), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Codex 5.3".into(), + }, +- stages: 1, ++ stages: 1, + }, + ], + } +@@ -1318,40 +1309,40 @@ mod runs { + pub(super) fn questions() -> Vec { + vec![ + ApiQuestion { +- id: "q-001".into(), +- text: "Should we proceed with the proposed changes?".into(), +- stage: "review".into(), +- question_type: QuestionType::YesNo, +- options: vec![ ++ id: "q-001".into(), ++ text: "Should we proceed with the proposed changes?".into(), ++ stage: "review".into(), ++ question_type: QuestionType::YesNo, ++ options: vec![ + ApiQuestionOption { +- key: "yes".into(), ++ key: "yes".into(), + label: "Yes".into(), + }, + ApiQuestionOption { +- key: "no".into(), ++ key: "no".into(), + label: "No".into(), + }, + ], +- allow_freeform: false, ++ allow_freeform: false, + timeout_seconds: None, + context_display: None, + }, + ApiQuestion { +- id: "q-002".into(), +- text: "Which approach do you prefer for the migration?".into(), +- stage: "migration".into(), +- question_type: QuestionType::MultipleChoice, +- options: vec![ ++ id: "q-002".into(), ++ text: "Which approach do you prefer for the migration?".into(), ++ stage: "migration".into(), ++ question_type: QuestionType::MultipleChoice, ++ options: vec![ + ApiQuestionOption { +- key: "incremental".into(), ++ key: "incremental".into(), + label: "Incremental migration".into(), + }, + ApiQuestionOption { +- key: "big_bang".into(), ++ key: "big_bang".into(), + label: "Big-bang rewrite".into(), + }, + ], +- allow_freeform: true, ++ allow_freeform: true, + timeout_seconds: None, + context_display: None, + }, +@@ -1400,62 +1391,62 @@ mod billing { + + pub(super) fn aggregate() -> AggregateBilling { + AggregateBilling { +- totals: AggregateBillingTotals { +- cache_read_tokens: None, ++ totals: AggregateBillingTotals { ++ cache_read_tokens: None, + cache_write_tokens: None, +- runs: 9, +- input_tokens: 643_860, +- output_tokens: 189_720, +- reasoning_tokens: None, +- runtime_secs: 3_501.0, +- total_tokens: 833_580, +- total_usd_micros: Some(20_340_000), ++ runs: 9, ++ input_tokens: 643_860, ++ output_tokens: 189_720, ++ reasoning_tokens: None, ++ runtime_secs: 3_501.0, ++ total_tokens: 833_580, ++ total_usd_micros: Some(20_340_000), + }, + by_model: vec![ + BillingByModel { + billing: BilledTokenCounts { +- cache_read_tokens: None, ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 304_020, +- output_tokens: 87_210, +- reasoning_tokens: None, +- total_tokens: 391_230, +- total_usd_micros: Some(12_150_000), ++ input_tokens: 304_020, ++ output_tokens: 87_210, ++ reasoning_tokens: None, ++ total_tokens: 391_230, ++ total_usd_micros: Some(12_150_000), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Opus 4.6".into(), + }, +- stages: 18, ++ stages: 18, + }, + BillingByModel { + billing: BilledTokenCounts { +- cache_read_tokens: None, ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 257_760, +- output_tokens: 78_750, +- reasoning_tokens: None, +- total_tokens: 336_510, +- total_usd_micros: Some(6_480_000), ++ input_tokens: 257_760, ++ output_tokens: 78_750, ++ reasoning_tokens: None, ++ total_tokens: 336_510, ++ total_usd_micros: Some(6_480_000), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Gemini 3.1".into(), + }, +- stages: 9, ++ stages: 9, + }, + BillingByModel { + billing: BilledTokenCounts { +- cache_read_tokens: None, ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 82_080, +- output_tokens: 23_760, +- reasoning_tokens: None, +- total_tokens: 105_840, +- total_usd_micros: Some(1_710_000), ++ input_tokens: 82_080, ++ output_tokens: 23_760, ++ reasoning_tokens: None, ++ total_tokens: 105_840, ++ total_usd_micros: Some(1_710_000), + }, +- model: ModelReference { ++ model: ModelReference { + id: "Codex 5.3".into(), + }, +- stages: 9, ++ stages: 9, + }, + ], + } +@@ -1478,26 +1469,25 @@ mod insights { + pub(super) fn history() -> Vec { + vec![ + HistoryEntry { +- id: "h1".into(), +- sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1".into(), ++ id: "h1".into(), ++ sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1".into(), + timestamp: ts("2025-09-15T13:58:00Z"), +- elapsed: 0.342, ++ elapsed: 0.342, + row_count: 6, + }, + HistoryEntry { +- id: "h2".into(), +- sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100".into(), ++ id: "h2".into(), ++ sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100".into(), + timestamp: ts("2025-09-15T13:52:00Z"), +- elapsed: 0.127, ++ elapsed: 0.127, + row_count: 23, + }, + HistoryEntry { +- id: "h3".into(), +- sql: +- "SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1" +- .into(), ++ id: "h3".into(), ++ sql: "SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1" ++ .into(), + timestamp: ts("2025-09-15T13:45:00Z"), +- elapsed: 0.531, ++ elapsed: 0.531, + row_count: 30, + }, + ] +diff --git a/lib/crates/fabro-server/src/diagnostics.rs b/lib/crates/fabro-server/src/diagnostics.rs +index 4682119e..77a081a7 100644 +--- a/lib/crates/fabro-server/src/diagnostics.rs ++++ b/lib/crates/fabro-server/src/diagnostics.rs +@@ -33,7 +33,7 @@ fn http_client_or_check( + + #[derive(Debug, Serialize)] + pub struct DiagnosticsReport { +- pub version: String, ++ pub version: String, + pub sections: Vec, + } + +@@ -92,14 +92,14 @@ pub async fn run_all(state: &AppState) -> DiagnosticsReport { + let crypto = check_crypto(state); + + DiagnosticsReport { +- version: FABRO_VERSION.to_string(), ++ version: FABRO_VERSION.to_string(), + sections: vec![ + CheckSection { +- title: "Credentials".to_string(), ++ title: "Credentials".to_string(), + checks: vec![llm, github, sandbox, brave], + }, + CheckSection { +- title: "Configuration".to_string(), ++ title: "Configuration".to_string(), + checks: vec![crypto], + }, + ], +@@ -111,20 +111,20 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { + Ok(result) => result, + Err(err) => { + return CheckResult { +- name: "LLM Providers".to_string(), +- status: CheckStatus::Error, +- summary: "failed to initialize".to_string(), +- details: vec![CheckDetail::new(err)], ++ name: "LLM Providers".to_string(), ++ status: CheckStatus::Error, ++ summary: "failed to initialize".to_string(), ++ details: vec![CheckDetail::new(err)], + remediation: Some("Check configured provider credentials".to_string()), + }; + } + }; + if result.client.provider_names().is_empty() && result.auth_issues.is_empty() { + return CheckResult { +- name: "LLM Providers".to_string(), +- status: CheckStatus::Error, +- summary: "none configured".to_string(), +- details: Vec::new(), ++ name: "LLM Providers".to_string(), ++ status: CheckStatus::Error, ++ summary: "none configured".to_string(), ++ details: Vec::new(), + remediation: Some("Set at least one provider API key".to_string()), + }; + } +@@ -185,19 +185,19 @@ fn probe_model(provider: Provider) -> String { + + async fn probe_llm_provider(client: &LlmClient, provider: Provider) -> Result<(), String> { + let request = Request { +- model: probe_model(provider), +- messages: vec![Message::user("hi")], +- provider: Some(provider.as_str().to_string()), +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: None, +- top_p: None, +- max_tokens: Some(16), +- stop_sequences: None, ++ model: probe_model(provider), ++ messages: vec![Message::user("hi")], ++ provider: Some(provider.as_str().to_string()), ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: None, ++ top_p: None, ++ max_tokens: Some(16), ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + }; + client +@@ -215,19 +215,19 @@ async fn check_github_app(state: &AppState) -> CheckResult { + Ok(Some(_)) => unreachable!("token strategy should not return app credentials"), + Ok(None) => { + return CheckResult { +- name: "GitHub Token".to_string(), +- status: CheckStatus::Warning, +- summary: "not configured".to_string(), +- details: Vec::new(), ++ name: "GitHub Token".to_string(), ++ status: CheckStatus::Warning, ++ summary: "not configured".to_string(), ++ details: Vec::new(), + remediation: Some("Run fabro install or set GITHUB_TOKEN".to_string()), + }; + } + Err(err) => { + return CheckResult { +- name: "GitHub Token".to_string(), +- status: CheckStatus::Error, +- summary: "missing token".to_string(), +- details: vec![CheckDetail::new(err.clone())], ++ name: "GitHub Token".to_string(), ++ status: CheckStatus::Error, ++ summary: "missing token".to_string(), ++ details: vec![CheckDetail::new(err.clone())], + remediation: Some(err), + }; + } +@@ -249,18 +249,18 @@ async fn check_github_app(state: &AppState) -> CheckResult { + + return match probe { + Ok(Ok(response)) if response.status().is_success() => CheckResult { +- name: "GitHub Token".to_string(), +- status: CheckStatus::Pass, +- summary: "configured".to_string(), +- details: Vec::new(), ++ name: "GitHub Token".to_string(), ++ status: CheckStatus::Pass, ++ summary: "configured".to_string(), ++ details: Vec::new(), + remediation: None, + }, + Ok(Ok(response)) if response.status() == fabro_http::StatusCode::UNAUTHORIZED => { + CheckResult { +- name: "GitHub Token".to_string(), +- status: CheckStatus::Error, +- summary: "token invalid".to_string(), +- details: vec![CheckDetail::new(format!( ++ name: "GitHub Token".to_string(), ++ status: CheckStatus::Error, ++ summary: "token invalid".to_string(), ++ details: vec![CheckDetail::new(format!( + "GitHub returned {}", + response.status() + ))], +@@ -268,27 +268,27 @@ async fn check_github_app(state: &AppState) -> CheckResult { + } + } + Ok(Ok(response)) => CheckResult { +- name: "GitHub Token".to_string(), +- status: CheckStatus::Error, +- summary: "connectivity error".to_string(), +- details: vec![CheckDetail::new(format!( ++ name: "GitHub Token".to_string(), ++ status: CheckStatus::Error, ++ summary: "connectivity error".to_string(), ++ details: vec![CheckDetail::new(format!( + "GitHub returned {}", + response.status() + ))], + remediation: Some("Check GitHub connectivity and GITHUB_TOKEN".to_string()), + }, + Ok(Err(err)) => CheckResult { +- name: "GitHub Token".to_string(), +- status: CheckStatus::Error, +- summary: "connectivity error".to_string(), +- details: vec![CheckDetail::new(err.to_string())], ++ name: "GitHub Token".to_string(), ++ status: CheckStatus::Error, ++ summary: "connectivity error".to_string(), ++ details: vec![CheckDetail::new(err.to_string())], + remediation: Some("Check GitHub connectivity and GITHUB_TOKEN".to_string()), + }, + Err(_) => CheckResult { +- name: "GitHub Token".to_string(), +- status: CheckStatus::Error, +- summary: "timeout".to_string(), +- details: vec![CheckDetail::new("GitHub probe timed out".to_string())], ++ name: "GitHub Token".to_string(), ++ status: CheckStatus::Error, ++ summary: "timeout".to_string(), ++ details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + remediation: Some("Check GitHub connectivity and GITHUB_TOKEN".to_string()), + }, + }; +@@ -318,20 +318,20 @@ async fn check_github_app(state: &AppState) -> CheckResult { + && !webhook_secret + { + return CheckResult { +- name: "GitHub App".to_string(), +- status: CheckStatus::Warning, +- summary: "not configured".to_string(), +- details: Vec::new(), ++ name: "GitHub App".to_string(), ++ status: CheckStatus::Warning, ++ summary: "not configured".to_string(), ++ details: Vec::new(), + remediation: Some("Configure GitHub App settings and secrets".to_string()), + }; + } + + let Some(app_id) = app_id else { + return CheckResult { +- name: "GitHub App".to_string(), +- status: CheckStatus::Error, +- summary: "missing app_id".to_string(), +- details: Vec::new(), ++ name: "GitHub App".to_string(), ++ status: CheckStatus::Error, ++ summary: "missing app_id".to_string(), ++ details: Vec::new(), + remediation: Some( + "Set [server.integrations.github].app_id in settings.toml".to_string(), + ), +@@ -339,10 +339,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { + }; + let Some(private_key_raw) = private_key_raw else { + return CheckResult { +- name: "GitHub App".to_string(), +- status: CheckStatus::Error, +- summary: "missing private key".to_string(), +- details: Vec::new(), ++ name: "GitHub App".to_string(), ++ status: CheckStatus::Error, ++ summary: "missing private key".to_string(), ++ details: Vec::new(), + remediation: Some("Set GITHUB_APP_PRIVATE_KEY".to_string()), + }; + }; +@@ -351,10 +351,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { + Ok(value) => value, + Err(err) => { + return CheckResult { +- name: "GitHub App".to_string(), +- status: CheckStatus::Error, +- summary: "private key invalid".to_string(), +- details: vec![CheckDetail::new(err.clone())], ++ name: "GitHub App".to_string(), ++ status: CheckStatus::Error, ++ summary: "private key invalid".to_string(), ++ details: vec![CheckDetail::new(err.clone())], + remediation: Some(err), + }; + } +@@ -364,10 +364,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { + Ok(jwt) => jwt, + Err(err) => { + return CheckResult { +- name: "GitHub App".to_string(), +- status: CheckStatus::Error, +- summary: "JWT signing failed".to_string(), +- details: vec![CheckDetail::new(err.clone())], ++ name: "GitHub App".to_string(), ++ status: CheckStatus::Error, ++ summary: "JWT signing failed".to_string(), ++ details: vec![CheckDetail::new(err.clone())], + remediation: Some(err), + }; + } +@@ -384,24 +384,24 @@ async fn check_github_app(state: &AppState) -> CheckResult { + .await; + match auth_result { + Ok(Ok(_app)) => CheckResult { +- name: "GitHub App".to_string(), +- status: CheckStatus::Pass, +- summary: slug.unwrap_or_else(|| "configured".to_string()), +- details: Vec::new(), ++ name: "GitHub App".to_string(), ++ status: CheckStatus::Pass, ++ summary: slug.unwrap_or_else(|| "configured".to_string()), ++ details: Vec::new(), + remediation: None, + }, + Ok(Err(err)) => CheckResult { +- name: "GitHub App".to_string(), +- status: CheckStatus::Error, +- summary: "connectivity error".to_string(), +- details: vec![CheckDetail::new(err.clone())], ++ name: "GitHub App".to_string(), ++ status: CheckStatus::Error, ++ summary: "connectivity error".to_string(), ++ details: vec![CheckDetail::new(err.clone())], + remediation: Some(err), + }, + Err(_) => CheckResult { +- name: "GitHub App".to_string(), +- status: CheckStatus::Error, +- summary: "timeout".to_string(), +- details: vec![CheckDetail::new("GitHub probe timed out".to_string())], ++ name: "GitHub App".to_string(), ++ status: CheckStatus::Error, ++ summary: "timeout".to_string(), ++ details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + remediation: Some("Check GitHub connectivity and credentials".to_string()), + }, + } +@@ -410,18 +410,18 @@ async fn check_github_app(state: &AppState) -> CheckResult { + fn check_sandbox(state: &AppState) -> CheckResult { + if state.vault_or_env("DAYTONA_API_KEY").is_some() { + CheckResult { +- name: "Sandbox".to_string(), +- status: CheckStatus::Pass, +- summary: "Daytona configured".to_string(), +- details: Vec::new(), ++ name: "Sandbox".to_string(), ++ status: CheckStatus::Pass, ++ summary: "Daytona configured".to_string(), ++ details: Vec::new(), + remediation: None, + } + } else { + CheckResult { +- name: "Sandbox".to_string(), +- status: CheckStatus::Warning, +- summary: "recommended, not configured".to_string(), +- details: Vec::new(), ++ name: "Sandbox".to_string(), ++ status: CheckStatus::Warning, ++ summary: "recommended, not configured".to_string(), ++ details: Vec::new(), + remediation: Some( + "Run `fabro secret set DAYTONA_API_KEY` to enable cloud sandbox execution" + .to_string(), +@@ -433,10 +433,10 @@ fn check_sandbox(state: &AppState) -> CheckResult { + async fn check_brave_search(state: &AppState) -> CheckResult { + let Some(api_key) = state.vault_or_env("BRAVE_SEARCH_API_KEY") else { + return CheckResult { +- name: "Web Search (Brave)".to_string(), +- status: CheckStatus::Warning, +- summary: "optional, not configured".to_string(), +- details: Vec::new(), ++ name: "Web Search (Brave)".to_string(), ++ status: CheckStatus::Warning, ++ summary: "optional, not configured".to_string(), ++ details: Vec::new(), + remediation: Some( + "Run `fabro secret set BRAVE_SEARCH_API_KEY` to enable web search".to_string(), + ), +@@ -459,31 +459,31 @@ async fn check_brave_search(state: &AppState) -> CheckResult { + + match probe { + Ok(Ok(response)) if response.status().is_success() => CheckResult { +- name: "Web Search (Brave)".to_string(), +- status: CheckStatus::Pass, +- summary: "configured and reachable".to_string(), +- details: Vec::new(), ++ name: "Web Search (Brave)".to_string(), ++ status: CheckStatus::Pass, ++ summary: "configured and reachable".to_string(), ++ details: Vec::new(), + remediation: None, + }, + Ok(Ok(response)) => CheckResult { +- name: "Web Search (Brave)".to_string(), +- status: CheckStatus::Warning, +- summary: format!("HTTP {}", response.status()), +- details: Vec::new(), ++ name: "Web Search (Brave)".to_string(), ++ status: CheckStatus::Warning, ++ summary: format!("HTTP {}", response.status()), ++ details: Vec::new(), + remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), + }, + Ok(Err(err)) => CheckResult { +- name: "Web Search (Brave)".to_string(), +- status: CheckStatus::Warning, +- summary: "connectivity error".to_string(), +- details: vec![CheckDetail::new(err.clone())], ++ name: "Web Search (Brave)".to_string(), ++ status: CheckStatus::Warning, ++ summary: "connectivity error".to_string(), ++ details: vec![CheckDetail::new(err.clone())], + remediation: Some(err), + }, + Err(_) => CheckResult { +- name: "Web Search (Brave)".to_string(), +- status: CheckStatus::Warning, +- summary: "timeout".to_string(), +- details: vec![CheckDetail::new( ++ name: "Web Search (Brave)".to_string(), ++ status: CheckStatus::Warning, ++ summary: "timeout".to_string(), ++ details: vec![CheckDetail::new( + "Web Search (Brave) probe timed out".to_string(), + )], + remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), +diff --git a/lib/crates/fabro-server/src/error.rs b/lib/crates/fabro-server/src/error.rs +index d2f849c0..cd28b4d0 100644 +--- a/lib/crates/fabro-server/src/error.rs ++++ b/lib/crates/fabro-server/src/error.rs +@@ -54,7 +54,7 @@ pub type Result = std::result::Result; + #[derive(Serialize)] + struct ErrorEntry { + status: String, +- title: String, ++ title: String, + detail: String, + } + +diff --git a/lib/crates/fabro-server/src/github_webhooks.rs b/lib/crates/fabro-server/src/github_webhooks.rs +index ca556dc9..5e0c159e 100644 +--- a/lib/crates/fabro-server/src/github_webhooks.rs ++++ b/lib/crates/fabro-server/src/github_webhooks.rs +@@ -103,7 +103,7 @@ fn parse_event_metadata(body: &[u8]) -> (String, String) { + + /// A running webhook listener that can be shut down. + pub struct WebhookListener { +- port: u16, ++ port: u16, + shutdown_tx: oneshot::Sender<()>, + } + +diff --git a/lib/crates/fabro-server/src/jwt_auth.rs b/lib/crates/fabro-server/src/jwt_auth.rs +index 3319fe45..f8884543 100644 +--- a/lib/crates/fabro-server/src/jwt_auth.rs ++++ b/lib/crates/fabro-server/src/jwt_auth.rs +@@ -21,15 +21,15 @@ pub enum CredentialSource { + + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct VerifiedAuth { +- pub login: String, +- pub auth_method: RunAuthMethod, ++ pub login: String, ++ pub auth_method: RunAuthMethod, + pub credential_source: CredentialSource, +- pub provider_id: Option, ++ pub provider_id: Option, + } + + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct ConfiguredAuth { +- pub methods: Vec, ++ pub methods: Vec, + pub dev_token: Option, + } + +@@ -138,10 +138,10 @@ fn authenticate_bearer(token: &str, config: &ConfiguredAuth) -> Result Result FromRequestParts for AuthenticatedService { + + /// Axum extractor that authenticates and extracts the request subject. + pub struct AuthenticatedSubject { +- pub login: Option, ++ pub login: Option, + pub auth_method: RunAuthMethod, + } + +@@ -210,7 +210,7 @@ impl FromRequestParts for AuthenticatedSubject { + + match auth_mode { + AuthMode::Disabled => Ok(Self { +- login: None, ++ login: None, + auth_method: RunAuthMethod::Disabled, + }), + AuthMode::Enabled(config) => { +@@ -220,7 +220,7 @@ impl FromRequestParts for AuthenticatedSubject { + authenticate_session(parts, config)? + }; + Ok(Self { +- login: Some(auth.login), ++ login: Some(auth.login), + auth_method: auth.auth_method, + }) + } +@@ -304,7 +304,7 @@ mod tests { + + fn dev_token_mode() -> AuthMode { + AuthMode::Enabled(ConfiguredAuth { +- methods: vec![ServerAuthMethod::DevToken], ++ methods: vec![ServerAuthMethod::DevToken], + dev_token: Some( + "fabro_dev_abababababababababababababababababababababababababababababababab" + .to_string(), +@@ -463,7 +463,7 @@ client_id = "Iv1.test" + #[tokio::test] + async fn cookie_session_reports_github_provenance() { + let app = subject_router(AuthMode::Enabled(ConfiguredAuth { +- methods: vec![ServerAuthMethod::Github], ++ methods: vec![ServerAuthMethod::Github], + dev_token: None, + })); + let response = app +diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs +index fe5a1a11..addbe12b 100644 +--- a/lib/crates/fabro-server/src/run_manifest.rs ++++ b/lib/crates/fabro-server/src/run_manifest.rs +@@ -40,14 +40,14 @@ use crate::server_secrets::auth_issue_message; + + #[derive(Clone)] + pub(crate) struct PreparedManifest { +- pub cwd: PathBuf, +- pub git: Option, +- pub root_source: String, +- pub run_id: Option, +- pub settings: SettingsLayer, +- pub target_path: PathBuf, +- pub workflow_bundle: WorkflowBundle, +- pub workflow_input: BundledWorkflow, ++ pub cwd: PathBuf, ++ pub git: Option, ++ pub root_source: String, ++ pub run_id: Option, ++ pub settings: SettingsLayer, ++ pub target_path: PathBuf, ++ pub workflow_bundle: WorkflowBundle, ++ pub workflow_input: BundledWorkflow, + pub working_directory: PathBuf, + } + +@@ -121,9 +121,9 @@ pub(crate) fn validate_prepared_manifest( + prepared: &PreparedManifest, + ) -> Result { + validate(ValidateInput { +- workflow: WorkflowInput::Bundled(prepared.workflow_input.clone()), +- settings: prepared.settings.clone(), +- cwd: prepared.cwd.clone(), ++ workflow: WorkflowInput::Bundled(prepared.workflow_input.clone()), ++ settings: prepared.settings.clone(), ++ cwd: prepared.cwd.clone(), + custom_transforms: Vec::new(), + }) + } +@@ -183,11 +183,14 @@ fn workflow_bundle_from_manifest( + .iter() + .map(|(key, entry)| (PathBuf::from(key), entry.content.clone())) + .collect::>(); +- Ok::<_, anyhow::Error>((PathBuf::from(path), BundledWorkflow { +- logical_path: PathBuf::from(path), +- source: workflow.source.clone(), +- files, +- })) ++ Ok::<_, anyhow::Error>(( ++ PathBuf::from(path), ++ BundledWorkflow { ++ logical_path: PathBuf::from(path), ++ source: workflow.source.clone(), ++ files, ++ }, ++ )) + }) + .collect::>>()?; + Ok(WorkflowBundle::new(workflows)) +@@ -222,8 +225,8 @@ fn manifest_args_layer(args: Option<&types::ManifestArgs>) -> SettingsLayer { + }; + + let model = (args.model.is_some() || args.provider.is_some()).then(|| RunModelLayer { +- provider: args.provider.as_deref().map(InterpString::parse), +- name: args.model.as_deref().map(InterpString::parse), ++ provider: args.provider.as_deref().map(InterpString::parse), ++ name: args.model.as_deref().map(InterpString::parse), + fallbacks: Vec::new(), + }); + let sandbox = +@@ -236,7 +239,7 @@ fn manifest_args_layer(args: Option<&types::ManifestArgs>) -> SettingsLayer { + let execution_has_any = + args.dry_run.is_some() || args.auto_approve.is_some() || args.no_retro.is_some(); + let execution = execution_has_any.then(|| RunExecutionLayer { +- mode: args ++ mode: args + .dry_run + .map(|d| if d { RunMode::DryRun } else { RunMode::Normal }), + approval: args.auto_approve.map(|a| { +@@ -246,7 +249,7 @@ fn manifest_args_layer(args: Option<&types::ManifestArgs>) -> SettingsLayer { + ApprovalMode::Prompt + } + }), +- retros: args.no_retro.map(|nr| !nr), ++ retros: args.no_retro.map(|nr| !nr), + }); + + let run_has_any = +@@ -345,7 +348,7 @@ async fn build_preflight_report( + if validated.has_errors() { + return Ok(( + CheckReport { +- title: "Run Preflight".into(), ++ title: "Run Preflight".into(), + sections: vec![CheckSection { + title: String::new(), + checks, +@@ -408,7 +411,7 @@ async fn build_preflight_report( + + Ok(( + CheckReport { +- title: "Run Preflight".into(), ++ title: "Run Preflight".into(), + sections: vec![CheckSection { + title: String::new(), + checks, +@@ -435,10 +438,10 @@ fn base_preflight_checks(prepared: &PreparedManifest, graph: &Graph) -> Vec Vec { + let _ = sandbox.cleanup().await; + checks.push(CheckResult { +- name: "Sandbox".into(), +- status: CheckStatus::Pass, +- summary: sandbox_provider.to_string(), +- details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], ++ name: "Sandbox".into(), ++ status: CheckStatus::Pass, ++ summary: sandbox_provider.to_string(), ++ details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], + remediation: None, + }); + true +@@ -538,10 +541,10 @@ async fn run_sandbox_check( + Err(err) => { + let _ = sandbox.cleanup().await; + checks.push(CheckResult { +- name: "Sandbox".into(), +- status: CheckStatus::Error, +- summary: "failed".into(), +- details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], ++ name: "Sandbox".into(), ++ status: CheckStatus::Error, ++ summary: "failed".into(), ++ details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], + remediation: Some(format!("Sandbox init failed: {err}")), + }); + false +@@ -549,10 +552,10 @@ async fn run_sandbox_check( + }, + Err(err) => { + checks.push(CheckResult { +- name: "Sandbox".into(), +- status: CheckStatus::Error, +- summary: "failed".into(), +- details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], ++ name: "Sandbox".into(), ++ status: CheckStatus::Error, ++ summary: "failed".into(), ++ details: vec![CheckDetail::new(format!("Provider: {sandbox_provider}"))], + remediation: Some(err), + }); + false +@@ -646,12 +649,10 @@ async fn run_llm_check( + } + Err(err) => { + checks.push(CheckResult { +- name: "LLM".into(), +- status: CheckStatus::Error, +- summary: model_id.clone(), +- details: vec![CheckDetail::new(format!( +- "Provider: {provider_name}" +- ))], ++ name: "LLM".into(), ++ status: CheckStatus::Error, ++ summary: model_id.clone(), ++ details: vec![CheckDetail::new(format!("Provider: {provider_name}"))], + remediation: Some(format!( + "Invalid provider \"{provider_name}\": {err}" + )), +@@ -664,10 +665,10 @@ async fn run_llm_check( + } + Err(err) => { + checks.push(CheckResult { +- name: "LLM".into(), +- status: CheckStatus::Error, +- summary: "initialization failed".into(), +- details: vec![], ++ name: "LLM".into(), ++ status: CheckStatus::Error, ++ summary: "initialization failed".into(), ++ details: vec![], + remediation: Some(format!("LLM client init failed: {err}")), + }); + false +@@ -715,15 +716,15 @@ fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { + fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { + DaytonaConfig { + auto_stop_interval: settings.auto_stop_interval, +- labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), +- snapshot: settings ++ labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), ++ snapshot: settings + .snapshot + .as_ref() + .map(|snapshot| DaytonaSnapshotSettings { +- name: snapshot.name.clone(), +- cpu: snapshot.cpu, +- memory: snapshot.memory_gb, +- disk: snapshot.disk_gb, ++ name: snapshot.name.clone(), ++ cpu: snapshot.cpu, ++ memory: snapshot.memory_gb, ++ disk: snapshot.disk_gb, + dockerfile: snapshot + .dockerfile + .as_ref() +@@ -736,14 +737,14 @@ fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { + } + }), + }), +- network: settings.network.as_ref().map(|network| match network { ++ network: settings.network.as_ref().map(|network| match network { + DaytonaNetworkLayer::Block => DaytonaNetwork::Block, + DaytonaNetworkLayer::AllowAll => DaytonaNetwork::AllowAll, + DaytonaNetworkLayer::AllowList { allow_list } => { + DaytonaNetwork::AllowList(allow_list.clone()) + } + }), +- skip_clone: settings.skip_clone, ++ skip_clone: settings.skip_clone, + } + } + +@@ -775,26 +776,26 @@ async fn run_github_token_check( + (Some(creds), Some(git)) => { + match mint_github_token(creds, &git.origin_url, &github_permissions).await { + Ok(_) => checks.push(CheckResult { +- name: "GitHub Token".into(), +- status: CheckStatus::Pass, +- summary: "minted".into(), +- details: perm_details, ++ name: "GitHub Token".into(), ++ status: CheckStatus::Pass, ++ summary: "minted".into(), ++ details: perm_details, + remediation: None, + }), + Err(err) => checks.push(CheckResult { +- name: "GitHub Token".into(), +- status: CheckStatus::Error, +- summary: "failed".into(), +- details: perm_details, ++ name: "GitHub Token".into(), ++ status: CheckStatus::Error, ++ summary: "failed".into(), ++ details: perm_details, + remediation: Some(format!("Failed to mint GitHub token: {err}")), + }), + } + } + _ => checks.push(CheckResult { +- name: "GitHub Token".into(), +- status: CheckStatus::Warning, +- summary: "skipped".into(), +- details: vec![], ++ name: "GitHub Token".into(), ++ status: CheckStatus::Warning, ++ summary: "skipped".into(), ++ details: vec![], + remediation: Some("No GitHub credentials or origin URL available".to_string()), + }), + } +@@ -842,11 +843,11 @@ fn preflight_response( + checks: report_to_api(report), + workflow: types::PreflightWorkflowSummary { + diagnostics: diagnostics_to_api(validated.diagnostics()), +- edges: i64::try_from(validated.graph().edges.len()).unwrap(), +- goal: validated.graph().goal().to_string(), +- graph_path: Some(target_path.display().to_string()), +- name: validated.graph().name.clone(), +- nodes: i64::try_from(validated.graph().nodes.len()).unwrap(), ++ edges: i64::try_from(validated.graph().edges.len()).unwrap(), ++ goal: validated.graph().goal().to_string(), ++ graph_path: Some(target_path.display().to_string()), ++ name: validated.graph().name.clone(), ++ nodes: i64::try_from(validated.graph().nodes.len()).unwrap(), + }, + } + } +@@ -857,14 +858,14 @@ fn diagnostics_to_api( + diagnostics + .iter() + .map(|diagnostic| types::WorkflowDiagnostic { +- edge: diagnostic ++ edge: diagnostic + .edge + .as_ref() + .map(|edge: &(String, String)| [edge.0.clone(), edge.1.clone()]), +- fix: diagnostic.fix.clone(), +- message: diagnostic.message.clone(), +- node_id: diagnostic.node_id.clone(), +- rule: diagnostic.rule.clone(), ++ fix: diagnostic.fix.clone(), ++ message: diagnostic.message.clone(), ++ node_id: diagnostic.node_id.clone(), ++ rule: diagnostic.rule.clone(), + severity: match diagnostic.severity { + Severity::Error => types::WorkflowDiagnosticSeverity::Error, + Severity::Warning => types::WorkflowDiagnosticSeverity::Warning, +@@ -884,7 +885,7 @@ fn report_to_api(report: &CheckReport) -> types::PreflightCheckReport { + .checks + .iter() + .map(|check| types::PreflightCheckResult { +- details: check ++ details: check + .details + .iter() + .map(|detail| types::PreflightCheckDetail { +@@ -892,20 +893,20 @@ fn report_to_api(report: &CheckReport) -> types::PreflightCheckReport { + warn: detail.warn, + }) + .collect(), +- name: check.name.clone(), ++ name: check.name.clone(), + remediation: check.remediation.clone(), +- status: match check.status { ++ status: match check.status { + CheckStatus::Pass => types::PreflightCheckResultStatus::Pass, + CheckStatus::Warning => types::PreflightCheckResultStatus::Warning, + CheckStatus::Error => types::PreflightCheckResultStatus::Error, + }, +- summary: check.summary.clone(), ++ summary: check.summary.clone(), + }) + .collect(), +- title: section.title.clone(), ++ title: section.title.clone(), + }) + .collect(), +- title: report.title.clone(), ++ title: report.title.clone(), + } + } + +@@ -915,35 +916,41 @@ mod tests { + + fn minimal_manifest() -> types::RunManifest { + types::RunManifest { +- args: None, +- configs: Vec::new(), +- cwd: "/tmp/project".to_string(), +- git: None, +- goal: None, +- run_id: None, +- target: types::ManifestTarget { ++ args: None, ++ configs: Vec::new(), ++ cwd: "/tmp/project".to_string(), ++ git: None, ++ goal: None, ++ run_id: None, ++ target: types::ManifestTarget { + identifier: "workflow.fabro".to_string(), +- path: "workflow.fabro".to_string(), ++ path: "workflow.fabro".to_string(), + }, +- version: 1, +- workflows: HashMap::from([("workflow.fabro".to_string(), types::ManifestWorkflow { +- config: None, +- files: HashMap::new(), +- source: +- "digraph Demo { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }" +- .to_string(), +- })]), ++ version: 1, ++ workflows: HashMap::from([( ++ "workflow.fabro".to_string(), ++ types::ManifestWorkflow { ++ config: None, ++ files: HashMap::new(), ++ source: ++ "digraph Demo { start [shape=Mdiamond] exit [shape=Msquare] start -> exit }" ++ .to_string(), ++ }, ++ )]), + } + } + + fn invalid_manifest() -> types::RunManifest { + types::RunManifest { +- workflows: HashMap::from([("workflow.fabro".to_string(), types::ManifestWorkflow { +- config: None, +- files: HashMap::new(), +- source: "digraph Invalid { exit [shape=Msquare] orphan exit -> orphan }" +- .to_string(), +- })]), ++ workflows: HashMap::from([( ++ "workflow.fabro".to_string(), ++ types::ManifestWorkflow { ++ config: None, ++ files: HashMap::new(), ++ source: "digraph Invalid { exit [shape=Msquare] orphan exit -> orphan }" ++ .to_string(), ++ }, ++ )]), + ..minimal_manifest() + } + } +@@ -967,15 +974,15 @@ root = "/srv/fabro" + ); + let mut manifest = minimal_manifest(); + manifest.args = Some(types::ManifestArgs { +- auto_approve: None, +- dry_run: Some(true), +- label: Vec::new(), +- model: None, +- no_retro: None, ++ auto_approve: None, ++ dry_run: Some(true), ++ label: Vec::new(), ++ model: None, ++ no_retro: None, + preserve_sandbox: None, +- provider: None, +- sandbox: None, +- verbose: None, ++ provider: None, ++ sandbox: None, ++ verbose: None, + }); + + let prepared = prepare_manifest_with_mode(&server_settings, &manifest, false).unwrap(); +@@ -1009,7 +1016,7 @@ app_id = "snapshotted-app-id" + let mut manifest = minimal_manifest(); + manifest.workflows.get_mut("workflow.fabro").unwrap().config = + Some(types::ManifestWorkflowConfig { +- path: "workflow.toml".to_string(), ++ path: "workflow.toml".to_string(), + source: r#" + _version = 1 + +@@ -1019,7 +1026,7 @@ script = "workflow-setup" + .to_string(), + }); + manifest.configs.push(types::ManifestConfig { +- path: Some("/tmp/home/.fabro/settings.toml".to_string()), ++ path: Some("/tmp/home/.fabro/settings.toml".to_string()), + source: Some( + r#" + _version = 1 +@@ -1032,7 +1039,7 @@ app_id = "snapshotted-app-id" + "# + .to_string(), + ), +- type_: types::ManifestConfigType::User, ++ type_: types::ManifestConfigType::User, + }); + + let prepared = prepare_manifest_with_mode(&server_settings, &manifest, true).unwrap(); +@@ -1041,9 +1048,10 @@ app_id = "snapshotted-app-id" + + // v2 merge matrix: run.prepare.steps replaces the whole list across + // layers, so the higher-precedence workflow layer wins over cli. +- assert_eq!(resolved_run.prepare.commands, vec![ +- "workflow-setup".to_string() +- ]); ++ assert_eq!( ++ resolved_run.prepare.commands, ++ vec!["workflow-setup".to_string()] ++ ); + assert_eq!( + resolved_server + .integrations +@@ -1084,7 +1092,7 @@ app_id = "snapshotted-app-id" + let state = crate::server::create_app_state(); + let mut manifest = minimal_manifest(); + manifest.configs.push(types::ManifestConfig { +- path: Some("/tmp/project/.fabro/project.toml".to_string()), ++ path: Some("/tmp/project/.fabro/project.toml".to_string()), + source: Some( + r" + _version = 1 +@@ -1094,7 +1102,7 @@ enabled = true + " + .to_string(), + ), +- type_: types::ManifestConfigType::Project, ++ type_: types::ManifestConfigType::Project, + }); + + let prepared = +@@ -1122,7 +1130,7 @@ enabled = true + let state = crate::server::create_app_state(); + let mut manifest = minimal_manifest(); + manifest.configs.push(types::ManifestConfig { +- path: Some("/tmp/project/.fabro/project.toml".to_string()), ++ path: Some("/tmp/project/.fabro/project.toml".to_string()), + source: Some( + r#" + _version = 1 +@@ -1132,7 +1140,7 @@ provider = "daytona" + "# + .to_string(), + ), +- type_: types::ManifestConfigType::Project, ++ type_: types::ManifestConfigType::Project, + }); + + let prepared = +diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs +index 659b09de..b76323a8 100644 +--- a/lib/crates/fabro-server/src/serve.rs ++++ b/lib/crates/fabro-server/src/serve.rs +@@ -223,7 +223,7 @@ fn bind_override_layer(bind: BindRequest) -> SettingsLayer { + }, + BindRequest::Tcp(address) => ServerListenLayer::Tcp { + address: Some(InterpString::parse(&address.to_string())), +- tls: None, ++ tls: None, + }, + BindRequest::TcpHost(_) => { + unreachable!("host-only bind requests are handled before building a settings override") +@@ -501,6 +501,10 @@ where + let mut watch_web_child = if watch_web { + let web_dir = std::env::current_dir()?.join("apps/fabro-web"); + info!(dir = %web_dir.display(), "Starting bun run dev (--watch-web)"); ++ #[expect( ++ clippy::disallowed_methods, ++ reason = "Intentional synchronous spawn before entering the async server loop" ++ )] + let child = std::process::Command::new("bun") + .args(["run", "dev"]) + .current_dir(&web_dir) +@@ -719,14 +723,14 @@ mod tests { + fn apply_runtime_settings_preserves_storage_dir() { + let base = SettingsLayer::default(); + let args = ServeArgs { +- bind: None, +- model: None, +- provider: None, +- sandbox: None, +- web: false, +- no_web: false, ++ bind: None, ++ model: None, ++ provider: None, ++ sandbox: None, ++ web: false, ++ no_web: false, + max_concurrent_runs: None, +- config: None, ++ config: None, + }; + + let resolved = apply_runtime_settings(&base, &args, &PathBuf::from("/srv/fabro-storage")); +@@ -751,14 +755,14 @@ enabled = false + ", + ); + let args = ServeArgs { +- bind: None, +- model: None, +- provider: None, +- sandbox: None, +- web: true, +- no_web: false, ++ bind: None, ++ model: None, ++ provider: None, ++ sandbox: None, ++ web: true, ++ no_web: false, + max_concurrent_runs: None, +- config: None, ++ config: None, + }; + + let resolved = apply_runtime_settings(&base, &args, &PathBuf::from("/srv/fabro")); +@@ -777,14 +781,14 @@ enabled = false + fn apply_runtime_settings_disables_web_from_cli_flag() { + let base = SettingsLayer::default(); + let args = ServeArgs { +- bind: None, +- model: None, +- provider: None, +- sandbox: None, +- web: false, +- no_web: true, ++ bind: None, ++ model: None, ++ provider: None, ++ sandbox: None, ++ web: false, ++ no_web: true, + max_concurrent_runs: None, +- config: None, ++ config: None, + }; + + let resolved = apply_runtime_settings(&base, &args, &PathBuf::from("/srv/fabro")); +diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs +index 86f0cc6a..8f6050f2 100644 +--- a/lib/crates/fabro-server/src/server.rs ++++ b/lib/crates/fabro-server/src/server.rs +@@ -126,7 +126,7 @@ pub fn default_page_limit() -> u32 { + #[derive(serde::Deserialize)] + pub struct PaginationParams { + #[serde(rename = "page[limit]", default = "default_page_limit")] +- pub limit: u32, ++ pub limit: u32, + #[serde(rename = "page[offset]", default)] + pub offset: u32, + } +@@ -134,13 +134,13 @@ pub struct PaginationParams { + #[derive(serde::Deserialize)] + struct ModelListParams { + #[serde(rename = "page[limit]", default = "default_page_limit")] +- limit: u32, ++ limit: u32, + #[serde(rename = "page[offset]", default)] +- offset: u32, ++ offset: u32, + #[serde(default)] + provider: Option, + #[serde(default)] +- query: Option, ++ query: Option, + } + + #[derive(serde::Deserialize)] +@@ -154,7 +154,7 @@ struct EventListParams { + #[serde(default)] + since_seq: Option, + #[serde(default)] +- limit: Option, ++ limit: Option, + } + + impl EventListParams { +@@ -193,7 +193,7 @@ struct ArtifactFilenameParams { + + #[derive(serde::Deserialize)] + struct SandboxFilesParams { +- path: String, ++ path: String, + #[serde(default)] + depth: Option, + } +@@ -221,22 +221,22 @@ impl ListResponse { + + /// Snapshot of a managed run. + struct ManagedRun { +- dot_source: String, +- status: RunStatus, +- error: Option, +- created_at: chrono::DateTime, +- enqueued_at: Instant, ++ dot_source: String, ++ status: RunStatus, ++ error: Option, ++ created_at: chrono::DateTime, ++ enqueued_at: Instant, + // Populated when running: +- answer_transport: Option, ++ answer_transport: Option, + accepted_questions: HashSet, +- event_tx: Option>, +- checkpoint: Option, +- cancel_tx: Option>, +- cancel_token: Option>, +- worker_pid: Option, +- worker_pgid: Option, +- run_dir: Option, +- execution_mode: RunExecutionMode, ++ event_tx: Option>, ++ checkpoint: Option, ++ cancel_tx: Option>, ++ cancel_token: Option>, ++ worker_pid: Option, ++ worker_pgid: Option, ++ run_dir: Option, ++ execution_mode: RunExecutionMode, + } + + #[derive(Clone, Copy)] +@@ -281,18 +281,18 @@ const MAX_MULTIPART_MANIFEST_BYTES: usize = 256 * 1024; + + #[derive(Clone)] + struct ArtifactUploadTokenKeys { +- encoding: Arc, +- decoding: Arc, ++ encoding: Arc, ++ decoding: Arc, + validation: Arc, + } + + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + struct ArtifactUploadClaims { +- iss: String, +- iat: u64, +- exp: u64, ++ iss: String, ++ iat: u64, ++ exp: u64, + run_id: String, +- scope: String, ++ scope: String, + } + + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +@@ -302,29 +302,29 @@ struct ArtifactBatchUploadManifest { + + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + struct ArtifactBatchUploadEntry { +- part: String, +- path: String, ++ part: String, ++ path: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- sha256: Option, ++ sha256: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + expected_bytes: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- content_type: Option, ++ content_type: Option, + } + + /// Per-model billing totals. + #[derive(Default)] + struct ModelBillingTotals { +- stages: i64, ++ stages: i64, + billing: BilledTokenCounts, + } + + /// In-memory aggregate billing counters, reset on server restart. + #[derive(Default)] + struct BillingAccumulator { +- total_runs: i64, ++ total_runs: i64, + total_runtime_secs: f64, +- by_model: HashMap, ++ by_model: HashMap, + } + + pub(crate) type RegistryFactoryOverride = +@@ -382,15 +382,15 @@ impl RunAnswerTransport { + + #[derive(Debug, Clone)] + struct LoadedPendingInterview { +- run_id: RunId, +- qid: String, ++ run_id: RunId, ++ qid: String, + question: InterviewQuestionRecord, + } + + #[derive(Clone)] + struct SlackService { +- client: SlackClient, +- app_token: String, ++ client: SlackClient, ++ app_token: String, + default_channel: String, + posted_messages: Arc>>, + thread_registry: Arc, +@@ -424,12 +424,12 @@ impl SlackService { + } + + let question = runtime_question_from_interview_record(&InterviewQuestionRecord { +- id: props.question_id.clone(), +- text: props.question.clone(), +- stage: props.stage.clone(), +- question_type: InterviewQuestionType::from_wire_name(&props.question_type), +- options: props.options.clone(), +- allow_freeform: props.allow_freeform, ++ id: props.question_id.clone(), ++ text: props.question.clone(), ++ stage: props.stage.clone(), ++ question_type: InterviewQuestionType::from_wire_name(&props.question_type), ++ options: props.options.clone(), ++ allow_freeform: props.allow_freeform, + timeout_seconds: props.timeout_seconds, + context_display: props.context_display.clone(), + }); +@@ -528,38 +528,38 @@ impl SlackService { + + /// Shared application state for the server. + pub struct AppState { +- runs: Mutex>, +- aggregate_billing: Mutex, +- store: Arc, +- artifact_store: ArtifactStore, ++ runs: Mutex>, ++ aggregate_billing: Mutex, ++ store: Arc, ++ artifact_store: ArtifactStore, + artifact_upload_tokens: ArtifactUploadTokenKeys, +- started_at: Instant, +- max_concurrent_runs: usize, +- scheduler_notify: Notify, +- global_event_tx: broadcast::Sender, ++ started_at: Instant, ++ max_concurrent_runs: usize, ++ scheduler_notify: Notify, ++ global_event_tx: broadcast::Sender, + +- pub(crate) vault: Arc>, +- pub(crate) server_secrets: ServerSecrets, ++ pub(crate) vault: Arc>, ++ pub(crate) server_secrets: ServerSecrets, + pub(crate) provider_credentials: ProviderCredentials, +- pub(crate) settings: Arc>, +- pub(crate) server_settings: RwLock>, +- pub(crate) local_daemon_mode: bool, +- shutting_down: AtomicBool, +- registry_factory_override: Option>, +- slack_service: Option>, +- slack_started: AtomicBool, ++ pub(crate) settings: Arc>, ++ pub(crate) server_settings: RwLock>, ++ pub(crate) local_daemon_mode: bool, ++ shutting_down: AtomicBool, ++ registry_factory_override: Option>, ++ slack_service: Option>, ++ slack_started: AtomicBool, + } + + pub(crate) struct AppStateConfig { +- pub(crate) settings: Arc>, ++ pub(crate) settings: Arc>, + pub(crate) registry_factory_override: Option>, +- pub(crate) max_concurrent_runs: usize, +- pub(crate) store: Arc, +- pub(crate) artifact_store: ArtifactStore, +- pub(crate) vault_path: PathBuf, +- pub(crate) server_env_path: PathBuf, +- pub(crate) local_daemon_mode: bool, +- pub(crate) env_lookup: EnvLookup, ++ pub(crate) max_concurrent_runs: usize, ++ pub(crate) store: Arc, ++ pub(crate) artifact_store: ArtifactStore, ++ pub(crate) vault_path: PathBuf, ++ pub(crate) server_env_path: PathBuf, ++ pub(crate) local_daemon_mode: bool, ++ pub(crate) env_lookup: EnvLookup, + } + + fn nonzero_i64(value: i64) -> Option { +@@ -568,26 +568,26 @@ fn nonzero_i64(value: i64) -> Option { + + fn api_billed_token_counts_from_domain(billing: &BilledTokenCounts) -> ApiBilledTokenCounts { + ApiBilledTokenCounts { +- cache_read_tokens: nonzero_i64(billing.cache_read_tokens), ++ cache_read_tokens: nonzero_i64(billing.cache_read_tokens), + cache_write_tokens: nonzero_i64(billing.cache_write_tokens), +- input_tokens: billing.input_tokens, +- output_tokens: billing.output_tokens, +- reasoning_tokens: nonzero_i64(billing.reasoning_tokens), +- total_tokens: billing.total_tokens, +- total_usd_micros: billing.total_usd_micros, ++ input_tokens: billing.input_tokens, ++ output_tokens: billing.output_tokens, ++ reasoning_tokens: nonzero_i64(billing.reasoning_tokens), ++ total_tokens: billing.total_tokens, ++ total_usd_micros: billing.total_usd_micros, + } + } + + fn api_billed_token_counts_from_usage(usage: &BilledModelUsage) -> ApiBilledTokenCounts { + let tokens = usage.tokens(); + ApiBilledTokenCounts { +- cache_read_tokens: nonzero_i64(tokens.cache_read_tokens), ++ cache_read_tokens: nonzero_i64(tokens.cache_read_tokens), + cache_write_tokens: nonzero_i64(tokens.cache_write_tokens), +- input_tokens: tokens.input_tokens, +- output_tokens: tokens.output_tokens, +- reasoning_tokens: nonzero_i64(tokens.reasoning_tokens), +- total_tokens: tokens.total_tokens(), +- total_usd_micros: usage.total_usd_micros, ++ input_tokens: tokens.input_tokens, ++ output_tokens: tokens.output_tokens, ++ reasoning_tokens: nonzero_i64(tokens.reasoning_tokens), ++ total_tokens: tokens.total_tokens(), ++ total_usd_micros: usage.total_usd_micros, + } + } + +@@ -690,11 +690,11 @@ impl AppState { + .map(|duration| duration.as_secs()) + .unwrap_or(0); + let claims = ArtifactUploadClaims { +- iss: ARTIFACT_UPLOAD_TOKEN_ISSUER.to_string(), +- iat: now, +- exp: now + ARTIFACT_UPLOAD_TOKEN_TTL_SECS, ++ iss: ARTIFACT_UPLOAD_TOKEN_ISSUER.to_string(), ++ iat: now, ++ exp: now + ARTIFACT_UPLOAD_TOKEN_TTL_SECS, + run_id: run_id.to_string(), +- scope: ARTIFACT_UPLOAD_TOKEN_SCOPE.to_string(), ++ scope: ARTIFACT_UPLOAD_TOKEN_SCOPE.to_string(), + }; + jsonwebtoken::encode( + &Header::new(Algorithm::HS256), +@@ -748,8 +748,8 @@ fn artifact_upload_token_keys() -> ArtifactUploadTokenKeys { + validation.set_issuer(&[ARTIFACT_UPLOAD_TOKEN_ISSUER]); + + ArtifactUploadTokenKeys { +- encoding: Arc::new(EncodingKey::from_secret(&secret)), +- decoding: Arc::new(DecodingKey::from_secret(&secret)), ++ encoding: Arc::new(EncodingKey::from_secret(&secret)), ++ decoding: Arc::new(DecodingKey::from_secret(&secret)), + validation: Arc::new(validation), + } + } +@@ -1223,20 +1223,20 @@ async fn get_system_info( + }; + + let response = SystemInfoResponse { +- version: Some(FABRO_VERSION.to_string()), +- git_sha: option_env!("FABRO_GIT_SHA").map(str::to_string), +- build_date: option_env!("FABRO_BUILD_DATE").map(str::to_string), +- os: Some(std::env::consts::OS.to_string()), +- arch: Some(std::env::consts::ARCH.to_string()), +- storage_engine: Some("slatedb".to_string()), +- storage_dir: Some(state.server_storage_dir().display().to_string()), +- uptime_secs: Some(to_i64(state.started_at.elapsed().as_secs())), +- runs: Some(SystemRunCounts { +- total: Some(to_i64(total_runs)), ++ version: Some(FABRO_VERSION.to_string()), ++ git_sha: option_env!("FABRO_GIT_SHA").map(str::to_string), ++ build_date: option_env!("FABRO_BUILD_DATE").map(str::to_string), ++ os: Some(std::env::consts::OS.to_string()), ++ arch: Some(std::env::consts::ARCH.to_string()), ++ storage_engine: Some("slatedb".to_string()), ++ storage_dir: Some(state.server_storage_dir().display().to_string()), ++ uptime_secs: Some(to_i64(state.started_at.elapsed().as_secs())), ++ runs: Some(SystemRunCounts { ++ total: Some(to_i64(total_runs)), + active: Some(to_i64(active_runs)), + }), + sandbox_provider: Some(system_sandbox_provider(&settings)), +- features: Some(system_features(&settings)), ++ features: Some(system_features(&settings)), + }; + (StatusCode::OK, Json(response)).into_response() + } +@@ -1250,7 +1250,7 @@ fn system_features(settings: &SettingsLayer) -> SystemFeatures { + .unwrap_or(false); + SystemFeatures { + session_sandboxes: Some(session_sandboxes), +- retros: Some(retros), ++ retros: Some(retros), + } + } + +@@ -1329,12 +1329,12 @@ async fn prune_runs( + return ( + StatusCode::OK, + Json(PruneRunsResponse { +- dry_run: Some(true), +- runs: Some(prune_plan.rows), +- total_count: Some(to_i64(prune_plan.run_ids.len())), ++ dry_run: Some(true), ++ runs: Some(prune_plan.rows), ++ total_count: Some(to_i64(prune_plan.run_ids.len())), + total_size_bytes: Some(to_i64(prune_plan.total_size_bytes)), +- deleted_count: Some(0), +- freed_bytes: Some(0), ++ deleted_count: Some(0), ++ freed_bytes: Some(0), + }), + ) + .into_response(); +@@ -1349,12 +1349,12 @@ async fn prune_runs( + ( + StatusCode::OK, + Json(PruneRunsResponse { +- dry_run: Some(false), +- runs: None, +- total_count: Some(to_i64(prune_plan.run_ids.len())), ++ dry_run: Some(false), ++ runs: None, ++ total_count: Some(to_i64(prune_plan.run_ids.len())), + total_size_bytes: Some(to_i64(prune_plan.total_size_bytes)), +- deleted_count: Some(to_i64(prune_plan.run_ids.len())), +- freed_bytes: Some(to_i64(prune_plan.total_size_bytes)), ++ deleted_count: Some(to_i64(prune_plan.run_ids.len())), ++ freed_bytes: Some(to_i64(prune_plan.total_size_bytes)), + }), + ) + .into_response() +@@ -1389,8 +1389,8 @@ async fn attach_events( + } + + struct PrunePlan { +- run_ids: Vec, +- rows: Vec, ++ run_ids: Vec, ++ rows: Vec, + total_size_bytes: u64, + } + +@@ -1418,12 +1418,12 @@ fn build_disk_usage_response( + } + if verbose { + run_rows.push(DiskUsageRunRow { +- run_id: Some(run.run_id().to_string()), ++ run_id: Some(run.run_id().to_string()), + workflow_name: Some(run.workflow_name()), +- status: Some(run.status().to_string()), +- start_time: Some(run.start_time()), +- size_bytes: Some(to_i64(size)), +- reclaimable: Some(!run.status().is_active()), ++ status: Some(run.status().to_string()), ++ start_time: Some(run.start_time()), ++ size_bytes: Some(to_i64(size)), ++ reclaimable: Some(!run.status().is_active()), + }); + } + } +@@ -1444,25 +1444,25 @@ fn build_disk_usage_response( + } + + Ok(DiskUsageResponse { +- summary: vec![ ++ summary: vec![ + DiskUsageSummaryRow { +- type_: Some("runs".to_string()), +- count: Some(to_i64(runs.len())), +- active: Some(to_i64(active_count)), +- size_bytes: Some(to_i64(total_run_size)), ++ type_: Some("runs".to_string()), ++ count: Some(to_i64(runs.len())), ++ active: Some(to_i64(active_count)), ++ size_bytes: Some(to_i64(total_run_size)), + reclaimable_bytes: Some(to_i64(reclaimable_run_size)), + }, + DiskUsageSummaryRow { +- type_: Some("logs".to_string()), +- count: Some(to_i64(log_count)), +- active: None, +- size_bytes: Some(to_i64(total_log_size)), ++ type_: Some("logs".to_string()), ++ count: Some(to_i64(log_count)), ++ active: None, ++ size_bytes: Some(to_i64(total_log_size)), + reclaimable_bytes: Some(to_i64(total_log_size)), + }, + ], +- total_size_bytes: Some(to_i64(total_run_size + total_log_size)), ++ total_size_bytes: Some(to_i64(total_run_size + total_log_size)), + total_reclaimable_bytes: Some(to_i64(reclaimable_run_size + total_log_size)), +- runs: verbose.then_some(run_rows), ++ runs: verbose.then_some(run_rows), + }) + } + +@@ -1512,10 +1512,10 @@ fn build_prune_plan( + let rows = filtered + .iter() + .map(|run| PruneRunEntry { +- run_id: Some(run.run_id().to_string()), +- dir_name: Some(run.dir_name.clone()), ++ run_id: Some(run.run_id().to_string()), ++ dir_name: Some(run.dir_name.clone()), + workflow_name: Some(run.workflow_name()), +- size_bytes: Some(to_i64(dir_size(&run.path))), ++ size_bytes: Some(to_i64(dir_size(&run.path))), + }) + .collect::>(); + let total_size_bytes = rows +@@ -1763,8 +1763,8 @@ async fn delete_secret_by_name( + #[derive(serde::Deserialize)] + struct GitHubRepoResponse { + default_branch: String, +- private: bool, +- permissions: Option, ++ private: bool, ++ permissions: Option, + } + + async fn get_github_repo( +@@ -2025,8 +2025,8 @@ async fn get_aggregate_billing( + .iter() + .map(|(model, totals)| BillingByModel { + billing: api_billed_token_counts_from_domain(&totals.billing), +- model: ModelReference { id: model.clone() }, +- stages: totals.stages, ++ model: ModelReference { id: model.clone() }, ++ stages: totals.stages, + }) + .collect(); + let total_billing = by_model +@@ -2045,15 +2045,15 @@ async fn get_aggregate_billing( + }); + let response = AggregateBilling { + totals: AggregateBillingTotals { +- cache_read_tokens: nonzero_i64(total_billing.cache_read_tokens), ++ cache_read_tokens: nonzero_i64(total_billing.cache_read_tokens), + cache_write_tokens: nonzero_i64(total_billing.cache_write_tokens), +- input_tokens: total_billing.input_tokens, +- output_tokens: total_billing.output_tokens, +- reasoning_tokens: nonzero_i64(total_billing.reasoning_tokens), +- runs: agg.total_runs, +- runtime_secs: agg.total_runtime_secs, +- total_tokens: total_billing.total_tokens, +- total_usd_micros: total_billing.total_usd_micros, ++ input_tokens: total_billing.input_tokens, ++ output_tokens: total_billing.output_tokens, ++ reasoning_tokens: nonzero_i64(total_billing.reasoning_tokens), ++ runs: agg.total_runs, ++ runtime_secs: agg.total_runtime_secs, ++ total_tokens: total_billing.total_tokens, ++ total_usd_micros: total_billing.total_usd_micros, + }, + by_model, + }; +@@ -2152,11 +2152,11 @@ async fn list_run_stages( + if let Some(next_id) = &checkpoint.next_node_id { + if run_is_active && next_id != "exit" && !checkpoint.completed_nodes.contains(next_id) { + stages.push(RunStage { +- id: next_id.clone(), +- name: next_id.clone(), +- status: ApiStageStatus::Running, ++ id: next_id.clone(), ++ name: next_id.clone(), ++ status: ApiStageStatus::Running, + duration_secs: None, +- dot_id: Some(next_id.clone()), ++ dot_id: Some(next_id.clone()), + }); + } + } +@@ -2187,16 +2187,16 @@ async fn get_run_billing( + let Some(checkpoint) = checkpoint else { + let empty = RunBilling { + by_model: Vec::new(), +- stages: Vec::new(), +- totals: RunBillingTotals { +- cache_read_tokens: None, ++ stages: Vec::new(), ++ totals: RunBillingTotals { ++ cache_read_tokens: None, + cache_write_tokens: None, +- input_tokens: 0, +- output_tokens: 0, +- reasoning_tokens: None, +- runtime_secs: 0.0, +- total_tokens: 0, +- total_usd_micros: None, ++ input_tokens: 0, ++ output_tokens: 0, ++ reasoning_tokens: None, ++ runtime_secs: 0.0, ++ total_tokens: 0, ++ total_usd_micros: None, + }, + }; + return (StatusCode::OK, Json(empty)).into_response(); +@@ -2236,7 +2236,7 @@ async fn get_run_billing( + model: ModelReference { id: model_id }, + runtime_secs: duration_ms as f64 / 1000.0, + stage: BillingStageRef { +- id: node_id.clone(), ++ id: node_id.clone(), + name: node_id.clone(), + }, + }); +@@ -2247,8 +2247,8 @@ async fn get_run_billing( + .into_iter() + .map(|(model, totals)| BillingByModel { + billing: api_billed_token_counts_from_domain(&totals.billing), +- model: ModelReference { id: model }, +- stages: totals.stages, ++ model: ModelReference { id: model }, ++ stages: totals.stages, + }) + .collect::>(); + +@@ -2918,7 +2918,7 @@ fn release_run_answer_claim(state: &AppState, run_id: RunId, qid: &str) { + + #[derive(Clone, Copy)] + struct LiveWorkerProcess { +- run_id: RunId, ++ run_id: RunId, + process_group_id: u32, + } + +@@ -3104,9 +3104,9 @@ async fn persist_cancelled_run_status(state: &AppState, run_id: RunId) -> anyhow + &run_store, + &run_id, + &workflow_event::Event::WorkflowRunFailed { +- error: WorkflowError::Cancelled, +- duration_ms: 0, +- reason: Some(WorkflowStatusReason::Cancelled), ++ error: WorkflowError::Cancelled, ++ duration_ms: 0, ++ reason: Some(WorkflowStatusReason::Cancelled), + git_commit_sha: None, + }, + ) +@@ -3425,41 +3425,41 @@ fn runtime_question_type(question_type: InterviewQuestionType) -> QuestionType { + + fn runtime_question_from_interview_record(question: &InterviewQuestionRecord) -> Question { + Question { +- id: question.id.clone(), +- text: question.text.clone(), +- question_type: runtime_question_type(question.question_type), +- options: question ++ id: question.id.clone(), ++ text: question.text.clone(), ++ question_type: runtime_question_type(question.question_type), ++ options: question + .options + .iter() + .map(|option| fabro_interview::QuestionOption { +- key: option.key.clone(), ++ key: option.key.clone(), + label: option.label.clone(), + }) + .collect(), +- allow_freeform: question.allow_freeform, +- default: None, ++ allow_freeform: question.allow_freeform, ++ default: None, + timeout_seconds: question.timeout_seconds, +- stage: question.stage.clone(), +- metadata: HashMap::new(), ++ stage: question.stage.clone(), ++ metadata: HashMap::new(), + context_display: question.context_display.clone(), + } + } + + fn api_question_from_interview_record(question: &InterviewQuestionRecord) -> ApiQuestion { + ApiQuestion { +- id: question.id.clone(), +- text: question.text.clone(), +- stage: question.stage.clone(), +- question_type: api_question_type(question.question_type), +- options: question ++ id: question.id.clone(), ++ text: question.text.clone(), ++ stage: question.stage.clone(), ++ question_type: api_question_type(question.question_type), ++ options: question + .options + .iter() + .map(|option| ApiQuestionOption { +- key: option.key.clone(), ++ key: option.key.clone(), + label: option.label.clone(), + }) + .collect(), +- allow_freeform: question.allow_freeform, ++ allow_freeform: question.allow_freeform, + timeout_seconds: question.timeout_seconds, + context_display: question.context_display.clone(), + } +@@ -3616,10 +3616,13 @@ fn answer_from_request( + .find(|option| option.key == key) + .cloned(); + match option { +- Some(option) => Ok(Answer::selected(key, fabro_interview::QuestionOption { +- key: option.key, +- label: option.label, +- })), ++ Some(option) => Ok(Answer::selected( ++ key, ++ fabro_interview::QuestionOption { ++ key: option.key, ++ label: option.label, ++ }, ++ )), + None => Err(ApiError::bad_request("Invalid option key.").into_response()), + } + } else if !req.selected_option_keys.is_empty() { +@@ -3713,12 +3716,12 @@ async fn create_run( + + fn run_provenance(headers: &HeaderMap, subject: &AuthenticatedSubject) -> RunProvenance { + RunProvenance { +- server: Some(RunServerProvenance { ++ server: Some(RunServerProvenance { + version: FABRO_VERSION.to_string(), + }), +- client: run_client_provenance(headers), ++ client: run_client_provenance(headers), + subject: Some(RunSubjectProvenance { +- login: subject.login.clone(), ++ login: subject.login.clone(), + auth_method: subject.auth_method, + }), + } +@@ -3922,13 +3925,13 @@ async fn start_run( + ( + StatusCode::OK, + Json(RunStatusResponse { +- id: id.to_string(), +- status: RunStatus::Queued, +- error: None, +- queue_position: None, +- status_reason: None, ++ id: id.to_string(), ++ status: RunStatus::Queued, ++ error: None, ++ queue_position: None, ++ status_reason: None, + pending_control: None, +- created_at: id.created_at(), ++ created_at: id.created_at(), + }), + ) + .into_response() +@@ -4270,9 +4273,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { + &run_store, + &run_id, + &workflow_event::Event::WorkflowRunFailed { +- error: WorkflowError::engine(err.to_string()), +- duration_ms: 0, +- reason: Some(WorkflowStatusReason::LaunchFailed), ++ error: WorkflowError::engine(err.to_string()), ++ duration_ms: 0, ++ reason: Some(WorkflowStatusReason::LaunchFailed), + git_commit_sha: None, + }, + ) +@@ -4291,9 +4294,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { + &run_store, + &run_id, + &workflow_event::Event::WorkflowRunFailed { +- error: WorkflowError::engine(message.clone()), +- duration_ms: 0, +- reason: Some(WorkflowStatusReason::LaunchFailed), ++ error: WorkflowError::engine(message.clone()), ++ duration_ms: 0, ++ reason: Some(WorkflowStatusReason::LaunchFailed), + git_commit_sha: None, + }, + ) +@@ -4320,9 +4323,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { + &run_store, + &run_id, + &workflow_event::Event::WorkflowRunFailed { +- error: WorkflowError::engine(message.clone()), +- duration_ms: 0, +- reason: Some(WorkflowStatusReason::LaunchFailed), ++ error: WorkflowError::engine(message.clone()), ++ duration_ms: 0, ++ reason: Some(WorkflowStatusReason::LaunchFailed), + git_commit_sha: None, + }, + ) +@@ -4340,9 +4343,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { + &run_store, + &run_id, + &workflow_event::Event::WorkflowRunFailed { +- error: WorkflowError::engine(message.clone()), +- duration_ms: 0, +- reason: Some(WorkflowStatusReason::LaunchFailed), ++ error: WorkflowError::engine(message.clone()), ++ duration_ms: 0, ++ reason: Some(WorkflowStatusReason::LaunchFailed), + git_commit_sha: None, + }, + ) +@@ -4372,9 +4375,9 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { + &run_store, + &run_id, + &workflow_event::Event::WorkflowRunFailed { +- error: WorkflowError::engine(err.to_string()), +- duration_ms: 0, +- reason: Some(WorkflowStatusReason::Terminated), ++ error: WorkflowError::engine(err.to_string()), ++ duration_ms: 0, ++ reason: Some(WorkflowStatusReason::Terminated), + git_commit_sha: None, + }, + ) +@@ -4996,11 +4999,11 @@ async fn list_run_artifacts( + data: entries + .into_iter() + .map(|entry| RunArtifactEntry { +- stage_id: entry.node.to_string(), +- node_slug: entry.node.node_id().to_string(), +- retry: entry.node.visit().cast_signed(), ++ stage_id: entry.node.to_string(), ++ node_slug: entry.node.node_id().to_string(), ++ retry: entry.node.visit().cast_signed(), + relative_path: entry.filename, +- size: entry.size.cast_signed(), ++ size: entry.size.cast_signed(), + }) + .collect(), + }) +@@ -5048,8 +5051,8 @@ enum ArtifactUploadContentType { + } + + struct ValidatedArtifactBatchEntry { +- path: String, +- sha256: Option, ++ path: String, ++ sha256: Option, + expected_bytes: Option, + } + +@@ -5181,11 +5184,14 @@ fn validate_artifact_batch_manifest( + } + } + if entries +- .insert(entry.part.clone(), ValidatedArtifactBatchEntry { +- path, +- sha256: entry.sha256.map(|value| value.to_ascii_lowercase()), +- expected_bytes: entry.expected_bytes, +- }) ++ .insert( ++ entry.part.clone(), ++ ValidatedArtifactBatchEntry { ++ path, ++ sha256: entry.sha256.map(|value| value.to_ascii_lowercase()), ++ expected_bytes: entry.expected_bytes, ++ }, ++ ) + .is_some() + { + return Err(bad_request_response(format!( +@@ -5505,7 +5511,7 @@ async fn generate_preview_url( + { + Ok(preview) => PreviewUrlResponse { + token: None, +- url: preview.url, ++ url: preview.url, + }, + Err(err) => { + return ApiError::new(StatusCode::CONFLICT, err).into_response(); +@@ -5515,7 +5521,7 @@ async fn generate_preview_url( + match sandbox.get_preview_link(port).await { + Ok(preview) => PreviewUrlResponse { + token: Some(preview.token), +- url: preview.url, ++ url: preview.url, + }, + Err(err) => { + return ApiError::new(StatusCode::CONFLICT, err).into_response(); +@@ -5566,8 +5572,8 @@ async fn list_sandbox_files( + .into_iter() + .map(|entry| SandboxFileEntry { + is_dir: entry.is_dir, +- name: entry.name, +- size: entry.size.map(u64::cast_signed), ++ name: entry.name, ++ size: entry.size.map(u64::cast_signed), + }) + .collect(), + }) +@@ -6192,9 +6198,9 @@ async fn create_completion( + req.tools + .into_iter() + .map(|t| ToolDefinition { +- name: t.name, ++ name: t.name, + description: t.description, +- parameters: t.parameters, ++ parameters: t.parameters, + }) + .collect(), + ) +@@ -6315,15 +6321,15 @@ async fn create_completion( + } + match generate_object(params, schema).await { + Ok(result) => Json(CompletionResponse { +- id: msg_id, +- model: model_id, +- message: convert_llm_message(&result.response.message), ++ id: msg_id, ++ model: model_id, ++ message: convert_llm_message(&result.response.message), + stop_reason: finish_reason_to_api_stop_reason(&result.finish_reason), +- usage: CompletionUsage { +- input_tokens: result.usage.input_tokens, ++ usage: CompletionUsage { ++ input_tokens: result.usage.input_tokens, + output_tokens: result.usage.output_tokens, + }, +- output: result.output, ++ output: result.output, + }) + .into_response(), + Err(e) => ApiError::new(StatusCode::BAD_GATEWAY, format!("LLM error: {e}")) +@@ -6332,15 +6338,15 @@ async fn create_completion( + } else { + match client.complete(&request).await { + Ok(response) => Json(CompletionResponse { +- id: response.id, +- model: response.model, +- message: convert_llm_message(&response.message), ++ id: response.id, ++ model: response.model, ++ message: convert_llm_message(&response.message), + stop_reason: finish_reason_to_api_stop_reason(&response.finish_reason), +- usage: CompletionUsage { +- input_tokens: response.usage.input_tokens, ++ usage: CompletionUsage { ++ input_tokens: response.usage.input_tokens, + output_tokens: response.usage.output_tokens, + }, +- output: None, ++ output: None, + }) + .into_response(), + Err(e) => ApiError::new(StatusCode::BAD_GATEWAY, format!("LLM error: {e}")) +@@ -6662,10 +6668,10 @@ type = "http" + + let vault = state.vault.read().await; + assert!(!vault.snapshot().contains_key("/tmp/test.pem")); +- assert_eq!(vault.file_secrets(), vec![( +- "/tmp/test.pem".to_string(), +- "pem-data".to_string() +- )]); ++ assert_eq!( ++ vault.file_secrets(), ++ vec![("/tmp/test.pem".to_string(), "pem-data".to_string())] ++ ); + } + + #[tokio::test] +@@ -6674,21 +6680,21 @@ type = "http" + let app = build_router(Arc::clone(&state), AuthMode::Disabled); + let credential = fabro_auth::AuthCredential { + provider: Provider::OpenAi, +- details: fabro_auth::AuthDetails::CodexOAuth { +- tokens: fabro_auth::OAuthTokens { +- access_token: "access".to_string(), ++ details: fabro_auth::AuthDetails::CodexOAuth { ++ tokens: fabro_auth::OAuthTokens { ++ access_token: "access".to_string(), + refresh_token: Some("refresh".to_string()), +- expires_at: chrono::DateTime::parse_from_rfc3339("2030-01-01T00:00:00Z") ++ expires_at: chrono::DateTime::parse_from_rfc3339("2030-01-01T00:00:00Z") + .unwrap() + .with_timezone(&chrono::Utc), + }, +- config: fabro_auth::OAuthConfig { +- auth_url: "https://auth.openai.com".to_string(), +- token_url: "https://auth.openai.com/oauth/token".to_string(), +- client_id: "client".to_string(), +- scopes: vec!["openid".to_string()], ++ config: fabro_auth::OAuthConfig { ++ auth_url: "https://auth.openai.com".to_string(), ++ token_url: "https://auth.openai.com/oauth/token".to_string(), ++ client_id: "client".to_string(), ++ scopes: vec!["openid".to_string()], + redirect_uri: Some("https://auth.openai.com/deviceauth/callback".to_string()), +- use_pkce: true, ++ use_pkce: true, + }, + account_id: Some("acct_123".to_string()), + }, +@@ -7117,11 +7123,14 @@ type = "http" + .iter() + .map(|model| model["id"].as_str().unwrap().to_string()) + .collect::>(); +- assert_eq!(model_ids, vec![ +- "gpt-5.2-codex".to_string(), +- "gpt-5.3-codex".to_string(), +- "gpt-5.3-codex-spark".to_string() +- ]); ++ assert_eq!( ++ model_ids, ++ vec![ ++ "gpt-5.2-codex".to_string(), ++ "gpt-5.3-codex".to_string(), ++ "gpt-5.3-codex-spark".to_string() ++ ] ++ ); + } + + #[tokio::test] +@@ -7162,7 +7171,7 @@ slug = "fabro" + false, + ), + AuthMode::Enabled(ConfiguredAuth { +- methods: vec![ServerAuthMethod::Github], ++ methods: vec![ServerAuthMethod::Github], + dev_token: None, + }), + ); +@@ -7363,18 +7372,18 @@ slug = "fabro" + async fn submit_pending_interview_answer_rejects_invalid_answer_shape() { + let state = create_app_state(); + let pending = LoadedPendingInterview { +- run_id: fixtures::RUN_1, +- qid: "q-1".to_string(), ++ run_id: fixtures::RUN_1, ++ qid: "q-1".to_string(), + question: InterviewQuestionRecord { +- id: "q-1".to_string(), +- text: "Approve deploy?".to_string(), +- stage: "gate".to_string(), +- question_type: InterviewQuestionType::MultipleChoice, +- options: vec![fabro_types::run_event::InterviewOption { +- key: "approve".to_string(), ++ id: "q-1".to_string(), ++ text: "Approve deploy?".to_string(), ++ stage: "gate".to_string(), ++ question_type: InterviewQuestionType::MultipleChoice, ++ options: vec![fabro_types::run_event::InterviewOption { ++ key: "approve".to_string(), + label: "Approve".to_string(), + }], +- allow_freeform: false, ++ allow_freeform: false, + timeout_seconds: None, + context_display: None, + }, +@@ -7490,7 +7499,7 @@ slug = "fabro" + let app = build_router( + Arc::clone(&state), + AuthMode::Enabled(ConfiguredAuth { +- methods: vec![ServerAuthMethod::DevToken], ++ methods: vec![ServerAuthMethod::DevToken], + dev_token: Some(DEV_TOKEN.to_string()), + }), + ); +@@ -7866,10 +7875,14 @@ slug = "fabro" + "content-type", + format!("multipart/form-data; boundary={boundary}"), + ) +- .body(multipart_body(boundary, &manifest, &[ +- ("file1", "src/lib.rs", source_bytes), +- ("file2", "logs/output.txt", log_bytes), +- ])) ++ .body(multipart_body( ++ boundary, ++ &manifest, ++ &[ ++ ("file1", "src/lib.rs", source_bytes), ++ ("file2", "logs/output.txt", log_bytes), ++ ], ++ )) + .unwrap(); + let response = app.clone().oneshot(req).await.unwrap(); + if response.status() != StatusCode::NO_CONTENT { +@@ -8704,32 +8717,42 @@ level = "debug" + async fn startup_reconciliation_marks_inflight_runs_terminal() { + let state = create_app_state(); + +- create_durable_run_with_events(&state, fixtures::RUN_1, &[ +- workflow_event::Event::RunSubmitted { +- reason: None, ++ create_durable_run_with_events( ++ &state, ++ fixtures::RUN_1, ++ &[workflow_event::Event::RunSubmitted { ++ reason: None, + definition_blob: None, +- }, +- ]) ++ }], ++ ) + .await; +- create_durable_run_with_events(&state, fixtures::RUN_2, &[ +- workflow_event::Event::RunSubmitted { +- reason: None, +- definition_blob: None, +- }, +- workflow_event::Event::RunStarting { reason: None }, +- workflow_event::Event::RunRunning { reason: None }, +- ]) ++ create_durable_run_with_events( ++ &state, ++ fixtures::RUN_2, ++ &[ ++ workflow_event::Event::RunSubmitted { ++ reason: None, ++ definition_blob: None, ++ }, ++ workflow_event::Event::RunStarting { reason: None }, ++ workflow_event::Event::RunRunning { reason: None }, ++ ], ++ ) + .await; +- create_durable_run_with_events(&state, fixtures::RUN_3, &[ +- workflow_event::Event::RunSubmitted { +- reason: None, +- definition_blob: None, +- }, +- workflow_event::Event::RunStarting { reason: None }, +- workflow_event::Event::RunRunning { reason: None }, +- workflow_event::Event::RunPaused, +- workflow_event::Event::RunCancelRequested { actor: None }, +- ]) ++ create_durable_run_with_events( ++ &state, ++ fixtures::RUN_3, ++ &[ ++ workflow_event::Event::RunSubmitted { ++ reason: None, ++ definition_blob: None, ++ }, ++ workflow_event::Event::RunStarting { reason: None }, ++ workflow_event::Event::RunRunning { reason: None }, ++ workflow_event::Event::RunPaused, ++ workflow_event::Event::RunCancelRequested { actor: None }, ++ ], ++ ) + .await; + + let reconciled = reconcile_incomplete_runs_on_startup(&state).await.unwrap(); +@@ -8777,14 +8800,18 @@ level = "debug" + let state = create_app_state(); + let run_id = fixtures::RUN_4; + +- create_durable_run_with_events(&state, run_id, &[ +- workflow_event::Event::RunSubmitted { +- reason: None, +- definition_blob: None, +- }, +- workflow_event::Event::RunStarting { reason: None }, +- workflow_event::Event::RunRunning { reason: None }, +- ]) ++ create_durable_run_with_events( ++ &state, ++ run_id, ++ &[ ++ workflow_event::Event::RunSubmitted { ++ reason: None, ++ definition_blob: None, ++ }, ++ workflow_event::Event::RunStarting { reason: None }, ++ workflow_event::Event::RunRunning { reason: None }, ++ ], ++ ) + .await; + + let temp_dir = tempfile::tempdir().unwrap(); +@@ -9151,15 +9178,19 @@ timeout = "30s" + let run_id = fixtures::RUN_1; + + // A run in Removing status should not appear on the board +- create_durable_run_with_events(&state, run_id, &[ +- workflow_event::Event::RunSubmitted { +- reason: None, +- definition_blob: None, +- }, +- workflow_event::Event::RunStarting { reason: None }, +- workflow_event::Event::RunRunning { reason: None }, +- workflow_event::Event::RunRemoving { reason: None }, +- ]) ++ create_durable_run_with_events( ++ &state, ++ run_id, ++ &[ ++ workflow_event::Event::RunSubmitted { ++ reason: None, ++ definition_blob: None, ++ }, ++ workflow_event::Event::RunStarting { reason: None }, ++ workflow_event::Event::RunRunning { reason: None }, ++ workflow_event::Event::RunRemoving { reason: None }, ++ ], ++ ) + .await; + + let req = Request::builder() +@@ -9185,34 +9216,42 @@ timeout = "30s" + let paused_id = fixtures::RUN_1; + let succeeded_id = fixtures::RUN_2; + +- create_durable_run_with_events(&state, paused_id, &[ +- workflow_event::Event::RunSubmitted { +- reason: None, +- definition_blob: None, +- }, +- workflow_event::Event::RunStarting { reason: None }, +- workflow_event::Event::RunRunning { reason: None }, +- workflow_event::Event::RunPaused, +- ]) ++ create_durable_run_with_events( ++ &state, ++ paused_id, ++ &[ ++ workflow_event::Event::RunSubmitted { ++ reason: None, ++ definition_blob: None, ++ }, ++ workflow_event::Event::RunStarting { reason: None }, ++ workflow_event::Event::RunRunning { reason: None }, ++ workflow_event::Event::RunPaused, ++ ], ++ ) + .await; +- create_durable_run_with_events(&state, succeeded_id, &[ +- workflow_event::Event::RunSubmitted { +- reason: None, +- definition_blob: None, +- }, +- workflow_event::Event::RunStarting { reason: None }, +- workflow_event::Event::RunRunning { reason: None }, +- workflow_event::Event::WorkflowRunCompleted { +- duration_ms: 1000, +- artifact_count: 0, +- status: "success".to_string(), +- reason: None, +- total_usd_micros: None, +- final_git_commit_sha: None, +- final_patch: None, +- billing: None, +- }, +- ]) ++ create_durable_run_with_events( ++ &state, ++ succeeded_id, ++ &[ ++ workflow_event::Event::RunSubmitted { ++ reason: None, ++ definition_blob: None, ++ }, ++ workflow_event::Event::RunStarting { reason: None }, ++ workflow_event::Event::RunRunning { reason: None }, ++ workflow_event::Event::WorkflowRunCompleted { ++ duration_ms: 1000, ++ artifact_count: 0, ++ status: "success".to_string(), ++ reason: None, ++ total_usd_micros: None, ++ final_git_commit_sha: None, ++ final_patch: None, ++ billing: None, ++ }, ++ ], ++ ) + .await; + + let req = Request::builder() +diff --git a/lib/crates/fabro-server/src/server_secrets.rs b/lib/crates/fabro-server/src/server_secrets.rs +index d296f0f0..79c05bb5 100644 +--- a/lib/crates/fabro-server/src/server_secrets.rs ++++ b/lib/crates/fabro-server/src/server_secrets.rs +@@ -18,9 +18,9 @@ pub(crate) enum Error { + } + + pub(crate) struct ServerSecrets { +- path: PathBuf, ++ path: PathBuf, + file_entries: HashMap, +- env_lookup: EnvLookup, ++ env_lookup: EnvLookup, + } + + impl ServerSecrets { +@@ -58,7 +58,7 @@ impl std::fmt::Debug for ServerSecrets { + + #[derive(Clone)] + pub(crate) struct ProviderCredentials { +- vault: Arc>, ++ vault: Arc>, + env_lookup: EnvLookup, + } + +@@ -119,7 +119,7 @@ impl ProviderCredentials { + } + + pub(crate) struct LlmClientResult { +- pub client: LlmClient, ++ pub client: LlmClient, + pub auth_issues: Vec<(Provider, ResolveError)>, + } + +@@ -168,9 +168,10 @@ mod tests { + (name == "OPENAI_API_KEY").then(|| "openai-key".to_string()) + }); + +- assert_eq!(credentials.configured_providers().await, vec![ +- Provider::OpenAi +- ]); ++ assert_eq!( ++ credentials.configured_providers().await, ++ vec![Provider::OpenAi] ++ ); + } + + #[tokio::test] +@@ -182,7 +183,7 @@ mod tests { + "anthropic", + &serde_json::to_string(&AuthCredential { + provider: Provider::Anthropic, +- details: AuthDetails::ApiKey { ++ details: AuthDetails::ApiKey { + key: "anthropic-key".to_string(), + }, + }) +@@ -194,8 +195,9 @@ mod tests { + let credentials = + ProviderCredentials::with_env_lookup(Arc::new(AsyncRwLock::new(vault)), |_| None); + +- assert_eq!(credentials.configured_providers().await, vec![ +- Provider::Anthropic +- ]); ++ assert_eq!( ++ credentials.configured_providers().await, ++ vec![Provider::Anthropic] ++ ); + } + } +diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs +index ed999c48..8601a11c 100644 +--- a/lib/crates/fabro-server/src/web_auth.rs ++++ b/lib/crates/fabro-server/src/web_auth.rs +@@ -22,21 +22,21 @@ const OAUTH_STATE_COOKIE_NAME: &str = "fabro_oauth_state"; + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct SessionCookie { +- pub v: u8, +- pub login: String, ++ pub v: u8, ++ pub login: String, + pub auth_method: RunAuthMethod, + pub provider_id: Option, +- pub name: String, +- pub email: String, +- pub avatar_url: String, +- pub user_url: String, +- pub iat: i64, +- pub exp: i64, ++ pub name: String, ++ pub email: String, ++ pub avatar_url: String, ++ pub user_url: String, ++ pub iat: i64, ++ pub exp: i64, + } + + #[derive(Deserialize)] + struct OAuthCallbackParams { +- code: String, ++ code: String, + state: String, + } + +@@ -57,21 +57,21 @@ struct AuthConfigResponse { + + #[derive(Serialize)] + struct AuthMeResponse { +- user: SessionUser, +- provider: String, ++ user: SessionUser, ++ provider: String, + #[serde(rename = "demoMode")] + demo_mode: bool, + } + + #[derive(Serialize)] + struct SessionUser { +- login: String, +- name: String, +- email: String, ++ login: String, ++ name: String, ++ email: String, + #[serde(rename = "avatarUrl")] + avatar_url: String, + #[serde(rename = "userUrl")] +- user_url: String, ++ user_url: String, + } + + #[derive(Deserialize)] +@@ -81,16 +81,16 @@ struct GitHubTokenResponse { + + #[derive(Deserialize)] + struct GitHubUser { +- id: i64, +- login: String, +- name: Option, ++ id: i64, ++ login: String, ++ name: Option, + avatar_url: String, + } + + #[derive(Deserialize)] + struct GitHubEmail { +- email: String, +- primary: bool, ++ email: String, ++ primary: bool, + verified: bool, + } + +@@ -245,16 +245,16 @@ async fn login_dev_token( + + let now = chrono::Utc::now(); + let session = SessionCookie { +- v: 1, +- login: "dev".to_string(), ++ v: 1, ++ login: "dev".to_string(), + auth_method: RunAuthMethod::DevToken, + provider_id: None, +- name: "Development User".to_string(), +- email: "dev@localhost".to_string(), +- avatar_url: "/logo.svg".to_string(), +- user_url: String::new(), +- iat: now.timestamp(), +- exp: (now + chrono::Duration::days(30)).timestamp(), ++ name: "Development User".to_string(), ++ email: "dev@localhost".to_string(), ++ avatar_url: "/logo.svg".to_string(), ++ user_url: String::new(), ++ iat: now.timestamp(), ++ exp: (now + chrono::Duration::days(30)).timestamp(), + }; + + let mut jar = CookieJar::new(); +@@ -334,14 +334,16 @@ async fn login_github( + } + + let state_token = format!("fabro-{}", ulid::Ulid::new()); +- let authorize_url = +- fabro_http::Url::parse_with_params("https://github.com/login/oauth/authorize", &[ ++ let authorize_url = fabro_http::Url::parse_with_params( ++ "https://github.com/login/oauth/authorize", ++ &[ + ("client_id", client_id.as_str()), + ("redirect_uri", &format!("{web_url}/auth/callback/github")), + ("scope", "read:user user:email"), + ("state", state_token.as_str()), +- ]) +- .expect("GitHub authorize URL should be valid"); ++ ], ++ ) ++ .expect("GitHub authorize URL should be valid"); + + debug!(redirect_uri = %format!("{web_url}/auth/callback/github"), "OAuth login redirecting to GitHub"); + +@@ -533,16 +535,16 @@ async fn callback_github( + .unwrap_or_default(); + let now = chrono::Utc::now(); + let session = SessionCookie { +- v: 1, +- login: profile.login.clone(), ++ v: 1, ++ login: profile.login.clone(), + auth_method: RunAuthMethod::Github, + provider_id: Some(profile.id), +- name: profile.name.unwrap_or_else(|| profile.login.clone()), +- email: primary_email, +- avatar_url: profile.avatar_url, +- user_url: format!("https://github.com/{}", profile.login), +- iat: now.timestamp(), +- exp: (now + chrono::Duration::days(30)).timestamp(), ++ name: profile.name.unwrap_or_else(|| profile.login.clone()), ++ email: primary_email, ++ avatar_url: profile.avatar_url, ++ user_url: format!("https://github.com/{}", profile.login), ++ iat: now.timestamp(), ++ exp: (now + chrono::Duration::days(30)).timestamp(), + }; + + info!(login = %session.login, "OAuth login succeeded"); +@@ -609,11 +611,11 @@ async fn auth_me(State(state): State>, headers: HeaderMap) -> Resp + .is_some_and(|cookie| cookie.value() == "1"); + Json(AuthMeResponse { + user: SessionUser { +- login: session.login, +- name: session.name, +- email: session.email, ++ login: session.login, ++ name: session.name, ++ email: session.email, + avatar_url: session.avatar_url, +- user_url: session.user_url, ++ user_url: session.user_url, + }, + provider: session_provider(session.auth_method).to_string(), + demo_mode, +@@ -662,14 +664,14 @@ mod tests { + + fn dev_token_auth_mode() -> AuthMode { + AuthMode::Enabled(ConfiguredAuth { +- methods: vec![ServerAuthMethod::DevToken], ++ methods: vec![ServerAuthMethod::DevToken], + dev_token: Some(DEV_TOKEN.to_string()), + }) + } + + fn github_auth_mode() -> AuthMode { + AuthMode::Enabled(ConfiguredAuth { +- methods: vec![ServerAuthMethod::Github], ++ methods: vec![ServerAuthMethod::Github], + dev_token: None, + }) + } +@@ -679,11 +681,11 @@ mod tests { + server: Some(ServerLayer { + web: Some(ServerWebLayer { + enabled: Some(true), +- url: Some(web_url.into()), ++ url: Some(web_url.into()), + }), + auth: Some(ServerAuthLayer { + methods: Some(vec![ServerAuthMethod::Github]), +- github: Some(ServerAuthGithubLayer { ++ github: Some(ServerAuthGithubLayer { + allowed_usernames: vec!["octocat".to_string()], + }), + }), +diff --git a/lib/crates/fabro-server/tests/it/api/tls.rs b/lib/crates/fabro-server/tests/it/api/tls.rs +index a666b548..d473ed14 100644 +--- a/lib/crates/fabro-server/tests/it/api/tls.rs ++++ b/lib/crates/fabro-server/tests/it/api/tls.rs +@@ -15,16 +15,16 @@ fn fixture_path(name: &str) -> PathBuf { + } + + struct PkiPaths { +- ca_cert: PathBuf, ++ ca_cert: PathBuf, + server_cert: PathBuf, +- server_key: PathBuf, ++ server_key: PathBuf, + } + + fn fixture_pki() -> PkiPaths { + PkiPaths { +- ca_cert: fixture_path("ca.crt"), ++ ca_cert: fixture_path("ca.crt"), + server_cert: fixture_path("server.crt"), +- server_key: fixture_path("server.key"), ++ server_key: fixture_path("server.key"), + } + } + +@@ -64,7 +64,7 @@ fn install_crypto_provider() { + fn tls_settings(pki: &PkiPaths) -> TlsConfig { + TlsConfig { + cert: InterpString::parse(&pki.server_cert.to_string_lossy()), +- key: InterpString::parse(&pki.server_key.to_string_lossy()), ++ key: InterpString::parse(&pki.server_key.to_string_lossy()), + } + } + +@@ -90,7 +90,7 @@ async fn tls_dev_token_auth_does_not_require_client_cert() { + let pki = fixture_pki(); + let dev_token = "fabro_dev_abababababababababababababababababababababababababababababababab"; + let auth_mode = AuthMode::Enabled(ConfiguredAuth { +- methods: vec![ServerAuthMethod::DevToken], ++ methods: vec![ServerAuthMethod::DevToken], + dev_token: Some(dev_token.to_string()), + }); + let addr = start_tls_server(&tls_settings(&pki), auth_mode).await; +diff --git a/lib/crates/fabro-server/tests/it/scenario/dry_run.rs b/lib/crates/fabro-server/tests/it/scenario/dry_run.rs +index b08ced4a..686ec78a 100644 +--- a/lib/crates/fabro-server/tests/it/scenario/dry_run.rs ++++ b/lib/crates/fabro-server/tests/it/scenario/dry_run.rs +@@ -15,7 +15,7 @@ use crate::helpers::{ + static ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + + struct ProxyPolicyGuard { +- _lock: MutexGuard<'static, ()>, ++ _lock: MutexGuard<'static, ()>, + previous: Option, + } + +diff --git a/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs +index 3bba3c4e..5e5bc26a 100644 +--- a/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs ++++ b/lib/crates/fabro-server/tests/it/scenario/lifecycle.rs +@@ -280,8 +280,11 @@ async fn cancel_at_human_gate_persists_cancelled_terminal_event() { + }) + .collect::>(); + +- assert_eq!(failed_reasons, vec![( +- Some("cancelled".to_string()), +- Some("Pipeline cancelled".to_string()) +- )]); ++ assert_eq!( ++ failed_reasons, ++ vec![( ++ Some("cancelled".to_string()), ++ Some("Pipeline cancelled".to_string()) ++ )] ++ ); + } +diff --git a/lib/crates/fabro-slack/src/blocks.rs b/lib/crates/fabro-slack/src/blocks.rs +index 7211b232..78427365 100644 +--- a/lib/crates/fabro-slack/src/blocks.rs ++++ b/lib/crates/fabro-slack/src/blocks.rs +@@ -65,8 +65,8 @@ pub fn question_to_blocks(run_id: &str, question_id: &str, question: &Question) + &opt.label, + &encode_action_value(&SlackActionPayload::Selected { + run_id: run_id.to_string(), +- qid: question_id.to_string(), +- key: opt.key.clone(), ++ qid: question_id.to_string(), ++ key: opt.key.clone(), + }), + ANSWER_ACTION_ID, + ) +@@ -165,15 +165,15 @@ mod tests { + let mut q = Question::new("Pick a language:", QuestionType::MultipleChoice); + q.options = vec![ + QuestionOption { +- key: "rs".to_string(), ++ key: "rs".to_string(), + label: "Rust".to_string(), + }, + QuestionOption { +- key: "ts".to_string(), ++ key: "ts".to_string(), + label: "TypeScript".to_string(), + }, + QuestionOption { +- key: "py".to_string(), ++ key: "py".to_string(), + label: "Python".to_string(), + }, + ]; +@@ -251,11 +251,11 @@ mod tests { + let mut q = Question::new("Select features:", QuestionType::MultiSelect); + q.options = vec![ + QuestionOption { +- key: "a".to_string(), ++ key: "a".to_string(), + label: "Auth".to_string(), + }, + QuestionOption { +- key: "b".to_string(), ++ key: "b".to_string(), + label: "Billing".to_string(), + }, + ]; +diff --git a/lib/crates/fabro-slack/src/client.rs b/lib/crates/fabro-slack/src/client.rs +index b3b38b49..5bb2d700 100644 +--- a/lib/crates/fabro-slack/src/client.rs ++++ b/lib/crates/fabro-slack/src/client.rs +@@ -6,14 +6,14 @@ const SLACK_API_BASE: &str = "https://slack.com/api"; + #[derive(Debug, Clone)] + pub struct PostedMessage { + pub channel_id: String, +- pub ts: String, ++ pub ts: String, + } + + #[derive(Clone)] + pub struct SlackClient { + bot_token: String, +- api_base: String, +- http: fabro_http::HttpClient, ++ api_base: String, ++ http: fabro_http::HttpClient, + } + + impl SlackClient { +@@ -109,7 +109,7 @@ pub fn parse_post_message_response(response: &Value) -> Result Option { + } + + pub struct SlackRuntimeOptions { +- pub config: SlackOptions, ++ pub config: SlackOptions, + pub credentials: SlackCredentials, + } + +diff --git a/lib/crates/fabro-slack/src/dispatch.rs b/lib/crates/fabro-slack/src/dispatch.rs +index b0c0f9f8..37832ada 100644 +--- a/lib/crates/fabro-slack/src/dispatch.rs ++++ b/lib/crates/fabro-slack/src/dispatch.rs +@@ -35,7 +35,7 @@ pub fn dispatch(envelope: &SocketEnvelope, thread_registry: &ThreadRegistry) -> + }; + DispatchAction::SubmitAnswer(SlackAnswerSubmission { + run_id: question_ref.run_id, +- qid: question_ref.qid, ++ qid: question_ref.qid, + answer: fabro_interview::Answer::text(text), + }) + } +@@ -55,8 +55,8 @@ mod tests { + let registry = ThreadRegistry::new(); + let envelope = SocketEnvelope { + envelope_type: "hello".to_string(), +- envelope_id: None, +- payload: None, ++ envelope_id: None, ++ payload: None, + }; + let action = dispatch(&envelope, ®istry); + assert!(matches!(action, DispatchAction::Connected)); +@@ -67,8 +67,8 @@ mod tests { + let registry = ThreadRegistry::new(); + let envelope = SocketEnvelope { + envelope_type: "interactive".to_string(), +- envelope_id: Some("env-1".to_string()), +- payload: Some(serde_json::json!({ ++ envelope_id: Some("env-1".to_string()), ++ payload: Some(serde_json::json!({ + "type": "block_actions", + "actions": [{ + "action_id": "interview.answer", +@@ -93,8 +93,8 @@ mod tests { + let registry = ThreadRegistry::new(); + let envelope = SocketEnvelope { + envelope_type: "interactive".to_string(), +- envelope_id: Some("env-2".to_string()), +- payload: Some(serde_json::json!({ ++ envelope_id: Some("env-2".to_string()), ++ payload: Some(serde_json::json!({ + "type": "view_submission" + })), + }; +@@ -107,8 +107,8 @@ mod tests { + let registry = ThreadRegistry::new(); + let envelope = SocketEnvelope { + envelope_type: "interactive".to_string(), +- envelope_id: Some("env-3".to_string()), +- payload: None, ++ envelope_id: Some("env-3".to_string()), ++ payload: None, + }; + let action = dispatch(&envelope, ®istry); + assert!(matches!(action, DispatchAction::Ignored)); +@@ -119,8 +119,8 @@ mod tests { + let registry = ThreadRegistry::new(); + let envelope = SocketEnvelope { + envelope_type: "disconnect".to_string(), +- envelope_id: None, +- payload: None, ++ envelope_id: None, ++ payload: None, + }; + let action = dispatch(&envelope, ®istry); + assert!(matches!(action, DispatchAction::Reconnect)); +@@ -131,8 +131,8 @@ mod tests { + let registry = ThreadRegistry::new(); + let envelope = SocketEnvelope { + envelope_type: "events_api".to_string(), +- envelope_id: Some("env-4".to_string()), +- payload: Some(serde_json::json!({ ++ envelope_id: Some("env-4".to_string()), ++ payload: Some(serde_json::json!({ + "event": { "type": "app_mention", "text": "hello" } + })), + }; +@@ -146,8 +146,8 @@ mod tests { + registry.register("1234.5678", "run-10", "q-10"); + let envelope = SocketEnvelope { + envelope_type: "events_api".to_string(), +- envelope_id: Some("env-5".to_string()), +- payload: Some(serde_json::json!({ ++ envelope_id: Some("env-5".to_string()), ++ payload: Some(serde_json::json!({ + "event": { + "type": "message", + "text": "https://github.com/org/repo", +@@ -175,8 +175,8 @@ mod tests { + let registry = ThreadRegistry::new(); + let envelope = SocketEnvelope { + envelope_type: "events_api".to_string(), +- envelope_id: Some("env-6".to_string()), +- payload: Some(serde_json::json!({ ++ envelope_id: Some("env-6".to_string()), ++ payload: Some(serde_json::json!({ + "event": { + "type": "message", + "text": "some reply", +@@ -194,8 +194,8 @@ mod tests { + let registry = ThreadRegistry::new(); + let envelope = SocketEnvelope { + envelope_type: "weird_type".to_string(), +- envelope_id: None, +- payload: None, ++ envelope_id: None, ++ payload: None, + }; + let action = dispatch(&envelope, ®istry); + assert!(matches!(action, DispatchAction::Ignored)); +diff --git a/lib/crates/fabro-slack/src/interaction.rs b/lib/crates/fabro-slack/src/interaction.rs +index 6a75c3c0..023292c5 100644 +--- a/lib/crates/fabro-slack/src/interaction.rs ++++ b/lib/crates/fabro-slack/src/interaction.rs +@@ -28,9 +28,9 @@ pub fn parse_interaction(payload: &Value) -> Option { + SlackActionPayload::Yes { .. } => Answer::yes(), + SlackActionPayload::No { .. } => Answer::no(), + SlackActionPayload::Selected { key, .. } => Answer { +- value: fabro_interview::AnswerValue::Selected(key), ++ value: fabro_interview::AnswerValue::Selected(key), + selected_option: None, +- text: None, ++ text: None, + }, + SlackActionPayload::SubmitMulti { .. } => return None, + }, +diff --git a/lib/crates/fabro-slack/src/payload.rs b/lib/crates/fabro-slack/src/payload.rs +index 12846658..c8cf2035 100644 +--- a/lib/crates/fabro-slack/src/payload.rs ++++ b/lib/crates/fabro-slack/src/payload.rs +@@ -4,13 +4,13 @@ use serde::{Deserialize, Serialize}; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct SlackQuestionRef { + pub run_id: String, +- pub qid: String, ++ pub qid: String, + } + + #[derive(Debug, Clone)] + pub struct SlackAnswerSubmission { + pub run_id: String, +- pub qid: String, ++ pub qid: String, + pub answer: Answer, + } + +@@ -19,20 +19,20 @@ pub struct SlackAnswerSubmission { + pub enum SlackActionPayload { + Yes { + run_id: String, +- qid: String, ++ qid: String, + }, + No { + run_id: String, +- qid: String, ++ qid: String, + }, + Selected { + run_id: String, +- qid: String, +- key: String, ++ qid: String, ++ key: String, + }, + SubmitMulti { + run_id: String, +- qid: String, ++ qid: String, + }, + } + +@@ -45,7 +45,7 @@ impl SlackActionPayload { + | Self::Selected { run_id, qid, .. } + | Self::SubmitMulti { run_id, qid } => SlackQuestionRef { + run_id: run_id.clone(), +- qid: qid.clone(), ++ qid: qid.clone(), + }, + } + } +@@ -64,8 +64,8 @@ mod tests { + fn action_payload_serializes_run_id_and_qid() { + let payload = SlackActionPayload::Selected { + run_id: "run_123".to_string(), +- qid: "q_123".to_string(), +- key: "approve".to_string(), ++ qid: "q_123".to_string(), ++ key: "approve".to_string(), + }; + let json = encode_action_value(&payload); + assert_eq!( +diff --git a/lib/crates/fabro-slack/src/socket.rs b/lib/crates/fabro-slack/src/socket.rs +index f99cc1c5..82253796 100644 +--- a/lib/crates/fabro-slack/src/socket.rs ++++ b/lib/crates/fabro-slack/src/socket.rs +@@ -5,8 +5,8 @@ use serde_json::Value; + pub struct SocketEnvelope { + #[serde(rename = "type")] + pub envelope_type: String, +- pub envelope_id: Option, +- pub payload: Option, ++ pub envelope_id: Option, ++ pub payload: Option, + } + + #[derive(Debug, Clone, Serialize)] +@@ -104,8 +104,8 @@ mod tests { + fn classify_hello() { + let envelope = SocketEnvelope { + envelope_type: "hello".to_string(), +- envelope_id: None, +- payload: None, ++ envelope_id: None, ++ payload: None, + }; + assert_eq!(classify_envelope(&envelope), SocketEventKind::Hello); + } +@@ -114,8 +114,8 @@ mod tests { + fn classify_interactive() { + let envelope = SocketEnvelope { + envelope_type: "interactive".to_string(), +- envelope_id: Some("e1".to_string()), +- payload: Some(serde_json::json!({"type": "block_actions"})), ++ envelope_id: Some("e1".to_string()), ++ payload: Some(serde_json::json!({"type": "block_actions"})), + }; + assert_eq!(classify_envelope(&envelope), SocketEventKind::Interactive); + } +@@ -124,8 +124,8 @@ mod tests { + fn classify_events_api() { + let envelope = SocketEnvelope { + envelope_type: "events_api".to_string(), +- envelope_id: Some("e2".to_string()), +- payload: Some(serde_json::json!({"event": {}})), ++ envelope_id: Some("e2".to_string()), ++ payload: Some(serde_json::json!({"event": {}})), + }; + assert_eq!(classify_envelope(&envelope), SocketEventKind::EventsApi); + } +@@ -134,8 +134,8 @@ mod tests { + fn classify_disconnect() { + let envelope = SocketEnvelope { + envelope_type: "disconnect".to_string(), +- envelope_id: None, +- payload: None, ++ envelope_id: None, ++ payload: None, + }; + assert_eq!(classify_envelope(&envelope), SocketEventKind::Disconnect); + } +@@ -144,8 +144,8 @@ mod tests { + fn classify_unknown() { + let envelope = SocketEnvelope { + envelope_type: "something_else".to_string(), +- envelope_id: None, +- payload: None, ++ envelope_id: None, ++ payload: None, + }; + assert_eq!(classify_envelope(&envelope), SocketEventKind::Unknown); + } +diff --git a/lib/crates/fabro-slack/src/threads.rs b/lib/crates/fabro-slack/src/threads.rs +index de14dfde..b83ea8a6 100644 +--- a/lib/crates/fabro-slack/src/threads.rs ++++ b/lib/crates/fabro-slack/src/threads.rs +@@ -19,10 +19,13 @@ impl ThreadRegistry { + self.ts_to_question + .lock() + .expect("thread registry lock poisoned") +- .insert(message_ts.to_string(), SlackQuestionRef { +- run_id: run_id.to_string(), +- qid: question_id.to_string(), +- }); ++ .insert( ++ message_ts.to_string(), ++ SlackQuestionRef { ++ run_id: run_id.to_string(), ++ qid: question_id.to_string(), ++ }, ++ ); + } + + pub fn resolve(&self, thread_ts: &str) -> Option { +@@ -84,7 +87,7 @@ mod tests { + registry.resolve("1234.5678"), + Some(SlackQuestionRef { + run_id: "run-1".to_string(), +- qid: "q-1".to_string(), ++ qid: "q-1".to_string(), + }) + ); + } +diff --git a/lib/crates/fabro-store/src/artifact_store.rs b/lib/crates/fabro-store/src/artifact_store.rs +index 3f3e72af..4cb4647e 100644 +--- a/lib/crates/fabro-store/src/artifact_store.rs ++++ b/lib/crates/fabro-store/src/artifact_store.rs +@@ -18,15 +18,15 @@ const STREAM_BUFFER_BYTES: usize = 1024 * 1024; + + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + pub struct NodeArtifact { +- pub node: StageId, ++ pub node: StageId, + pub filename: String, +- pub size: u64, ++ pub size: u64, + } + + #[derive(Clone)] + pub struct ArtifactStore { + object_store: Arc, +- prefix: ObjectPath, ++ prefix: ObjectPath, + } + + impl std::fmt::Debug for ArtifactStore { +@@ -332,16 +332,18 @@ mod tests { + store.get(&run_id, &node, filename).await.unwrap(), + Some(Bytes::from_static(b"hello")) + ); +- assert_eq!(store.list_for_node(&run_id, &node).await.unwrap(), vec![ +- filename.to_string() +- ]); +- assert_eq!(store.list_for_run(&run_id).await.unwrap(), vec![ +- NodeArtifact { ++ assert_eq!( ++ store.list_for_node(&run_id, &node).await.unwrap(), ++ vec![filename.to_string()] ++ ); ++ assert_eq!( ++ store.list_for_run(&run_id).await.unwrap(), ++ vec![NodeArtifact { + node, + filename: filename.to_string(), + size: 5, +- } +- ]); ++ }] ++ ); + } + + #[tokio::test] +diff --git a/lib/crates/fabro-store/src/keys.rs b/lib/crates/fabro-store/src/keys.rs +index 9a7981c0..98c70960 100644 +--- a/lib/crates/fabro-store/src/keys.rs ++++ b/lib/crates/fabro-store/src/keys.rs +@@ -141,12 +141,10 @@ mod tests { + let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); + let key = run_event_key(&run_id, 7, 123); + let segments: Vec<&str> = SlateKey::segments(key.as_str()).collect(); +- assert_eq!(segments, [ +- "runs", +- "01JT56VE4Z5NZ814GZN2JZD65A", +- "events", +- "000007-123" +- ]); ++ assert_eq!( ++ segments, ++ ["runs", "01JT56VE4Z5NZ814GZN2JZD65A", "events", "000007-123"] ++ ); + } + + #[test] +@@ -154,13 +152,16 @@ mod tests { + let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(); + let key = runs_index_by_start_key(&run_id); + let segments: Vec<&str> = SlateKey::segments(key.as_str()).collect(); +- assert_eq!(segments, [ +- "runs", +- "_index", +- "by-start", +- &run_id.created_at().format("%Y-%m-%d").to_string(), +- &run_id.to_string(), +- ]); ++ assert_eq!( ++ segments, ++ [ ++ "runs", ++ "_index", ++ "by-start", ++ &run_id.created_at().format("%Y-%m-%d").to_string(), ++ &run_id.to_string(), ++ ] ++ ); + } + + #[test] +diff --git a/lib/crates/fabro-store/src/lib.rs b/lib/crates/fabro-store/src/lib.rs +index 0cff9c5d..f3ec04e6 100644 +--- a/lib/crates/fabro-store/src/lib.rs ++++ b/lib/crates/fabro-store/src/lib.rs +@@ -17,5 +17,5 @@ pub use types::{EventEnvelope, EventPayload, RunSummary}; + #[derive(Debug, Default, Clone, PartialEq, Eq)] + pub struct ListRunsQuery { + pub start: Option>, +- pub end: Option>, ++ pub end: Option>, + } +diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs +index cb0be518..4c73de2f 100644 +--- a/lib/crates/fabro-store/src/run_state.rs ++++ b/lib/crates/fabro-store/src/run_state.rs +@@ -20,48 +20,48 @@ use crate::{Error, EventEnvelope, Result, RunSummary, StageId}; + #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] + #[serde(default)] + pub struct RunProjection { +- pub run: Option, +- pub graph_source: Option, +- pub start: Option, +- pub status: Option, +- pub pending_control: Option, +- pub checkpoint: Option, +- pub checkpoints: Vec<(u32, Checkpoint)>, +- pub conclusion: Option, +- pub retro: Option, +- pub retro_prompt: Option, +- pub retro_response: Option, +- pub sandbox: Option, +- pub final_patch: Option, +- pub pull_request: Option, ++ pub run: Option, ++ pub graph_source: Option, ++ pub start: Option, ++ pub status: Option, ++ pub pending_control: Option, ++ pub checkpoint: Option, ++ pub checkpoints: Vec<(u32, Checkpoint)>, ++ pub conclusion: Option, ++ pub retro: Option, ++ pub retro_prompt: Option, ++ pub retro_response: Option, ++ pub sandbox: Option, ++ pub final_patch: Option, ++ pub pull_request: Option, + pub pending_interviews: BTreeMap, +- nodes: HashMap, ++ nodes: HashMap, + } + + #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] + pub struct PendingInterviewRecord { +- pub question: InterviewQuestionRecord, ++ pub question: InterviewQuestionRecord, + pub started_at: Option>, + } + + #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] + pub struct NodeState { +- pub prompt: Option, +- pub response: Option, +- pub status: Option, +- pub provider_used: Option, +- pub diff: Option, ++ pub prompt: Option, ++ pub response: Option, ++ pub status: Option, ++ pub provider_used: Option, ++ pub diff: Option, + pub script_invocation: Option, +- pub script_timing: Option, +- pub parallel_results: Option, +- pub stdout: Option, +- pub stderr: Option, ++ pub script_timing: Option, ++ pub parallel_results: Option, ++ pub stdout: Option, ++ pub stderr: Option, + } + + #[derive(Debug, Clone, Default)] + pub(crate) struct EventProjectionCache { + pub last_seq: u32, +- pub state: RunProjection, ++ pub state: RunProjection, + } + + impl RunProjection { +@@ -172,11 +172,11 @@ impl RunProjection { + } + EventBody::SandboxInitialized(props) => { + self.sandbox = Some(SandboxRecord { +- provider: props.provider.clone(), +- working_directory: props.working_directory.clone(), +- identifier: props.identifier.clone(), ++ provider: props.provider.clone(), ++ working_directory: props.working_directory.clone(), ++ identifier: props.identifier.clone(), + host_working_directory: props.host_working_directory.clone(), +- container_mount_point: props.container_mount_point.clone(), ++ container_mount_point: props.container_mount_point.clone(), + }); + } + EventBody::RetroStarted(props) => { +@@ -193,35 +193,37 @@ impl RunProjection { + } + EventBody::PullRequestCreated(props) => { + self.pull_request = Some(PullRequestRecord { +- html_url: props.pr_url.clone(), +- number: props.pr_number, +- owner: props.owner.clone(), +- repo: props.repo.clone(), ++ html_url: props.pr_url.clone(), ++ number: props.pr_number, ++ owner: props.owner.clone(), ++ repo: props.repo.clone(), + base_branch: props.base_branch.clone(), + head_branch: props.head_branch.clone(), +- title: props.title.clone(), ++ title: props.title.clone(), + }); + } + EventBody::InterviewStarted(props) => { + if props.question_id.is_empty() { + return Ok(()); + } +- self.pending_interviews +- .insert(props.question_id.clone(), PendingInterviewRecord { +- question: InterviewQuestionRecord { +- id: props.question_id.clone(), +- text: props.question.clone(), +- stage: props.stage.clone(), +- question_type: InterviewQuestionType::from_wire_name( ++ self.pending_interviews.insert( ++ props.question_id.clone(), ++ PendingInterviewRecord { ++ question: InterviewQuestionRecord { ++ id: props.question_id.clone(), ++ text: props.question.clone(), ++ stage: props.stage.clone(), ++ question_type: InterviewQuestionType::from_wire_name( + &props.question_type, + ), +- options: props.options.clone(), +- allow_freeform: props.allow_freeform, ++ options: props.options.clone(), ++ allow_freeform: props.allow_freeform, + timeout_seconds: props.timeout_seconds, + context_display: props.context_display.clone(), + }, + started_at: Some(ts), +- }); ++ }, ++ ); + } + EventBody::InterviewCompleted(props) => { + if !props.question_id.is_empty() { +@@ -511,21 +513,21 @@ fn stage_visit( + + fn stage_outcome_from_props(props: &StageCompletedProps) -> Outcome> { + Outcome { +- status: props.status.clone(), +- preferred_label: props.preferred_label.clone(), ++ status: props.status.clone(), ++ preferred_label: props.preferred_label.clone(), + suggested_next_ids: props.suggested_next_ids.clone(), +- context_updates: props ++ context_updates: props + .context_updates + .clone() + .unwrap_or_default() + .into_iter() + .collect(), +- jump_to_node: props.jump_to_node.clone(), +- notes: props.notes.clone(), +- failure: props.failure.clone(), +- usage: props.billing.clone(), +- files_touched: props.files_touched.clone(), +- duration_ms: Some(props.duration_ms), ++ jump_to_node: props.jump_to_node.clone(), ++ notes: props.notes.clone(), ++ failure: props.failure.clone(), ++ usage: props.billing.clone(), ++ files_touched: props.files_touched.clone(), ++ duration_ms: Some(props.duration_ms), + } + } + +@@ -682,25 +684,31 @@ mod tests { + fn set_node_round_trips_through_json() { + let mut state = RunProjection { + pending_control: Some(RunControlAction::Unpause), +- checkpoints: vec![(7, Checkpoint { +- timestamp: "2026-04-07T12:00:00Z".parse().unwrap(), +- current_node: "build".to_string(), +- completed_nodes: vec!["build".to_string()], +- node_retries: HashMap::new(), +- context_values: HashMap::new(), +- node_outcomes: HashMap::new(), +- next_node_id: None, +- git_commit_sha: None, +- loop_failure_signatures: HashMap::new(), +- restart_failure_signatures: HashMap::new(), +- node_visits: HashMap::from([("build".to_string(), 2usize)]), +- })], ++ checkpoints: vec![( ++ 7, ++ Checkpoint { ++ timestamp: "2026-04-07T12:00:00Z".parse().unwrap(), ++ current_node: "build".to_string(), ++ completed_nodes: vec!["build".to_string()], ++ node_retries: HashMap::new(), ++ context_values: HashMap::new(), ++ node_outcomes: HashMap::new(), ++ next_node_id: None, ++ git_commit_sha: None, ++ loop_failure_signatures: HashMap::new(), ++ restart_failure_signatures: HashMap::new(), ++ node_visits: HashMap::from([("build".to_string(), 2usize)]), ++ }, ++ )], + ..RunProjection::default() + }; +- state.set_node(StageId::new("build", 2), NodeState { +- stdout: Some("done".to_string()), +- ..NodeState::default() +- }); ++ state.set_node( ++ StageId::new("build", 2), ++ NodeState { ++ stdout: Some("done".to_string()), ++ ..NodeState::default() ++ }, ++ ); + + let round_tripped: RunProjection = + serde_json::from_value(serde_json::to_value(&state).unwrap()).unwrap(); +@@ -727,21 +735,21 @@ mod tests { + .apply_event(&test_event( + 1, + EventBody::InterviewStarted(InterviewStartedProps { +- question_id: "q-1".to_string(), +- question: "Approve deploy?".to_string(), +- stage: "gate".to_string(), +- question_type: "multiple_choice".to_string(), +- options: vec![ ++ question_id: "q-1".to_string(), ++ question: "Approve deploy?".to_string(), ++ stage: "gate".to_string(), ++ question_type: "multiple_choice".to_string(), ++ options: vec![ + InterviewOption { +- key: "approve".to_string(), ++ key: "approve".to_string(), + label: "Approve".to_string(), + }, + InterviewOption { +- key: "revise".to_string(), ++ key: "revise".to_string(), + label: "Revise".to_string(), + }, + ], +- allow_freeform: true, ++ allow_freeform: true, + timeout_seconds: Some(30.0), + context_display: Some("Latest draft".to_string()), + }), +@@ -772,8 +780,8 @@ mod tests { + 2, + EventBody::InterviewCompleted(InterviewCompletedProps { + question_id: "q-1".to_string(), +- question: "Approve deploy?".to_string(), +- answer: "approve".to_string(), ++ question: "Approve deploy?".to_string(), ++ answer: "approve".to_string(), + duration_ms: 42, + }), + Some("gate"), +@@ -793,7 +801,7 @@ mod tests { + RunBlobId::new(br#"{"version":1,"workflow_path":"workflow.fabro"}"#).to_string(); + let events = vec![ + EventEnvelope { +- seq: 1, ++ seq: 1, + payload: EventPayload::new( + json!({ + "id": "evt-run-created", +@@ -819,7 +827,7 @@ mod tests { + .unwrap(), + }, + EventEnvelope { +- seq: 2, ++ seq: 2, + payload: EventPayload::new( + json!({ + "id": "evt-run-submitted", +diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs +index d6801631..126ae4e3 100644 +--- a/lib/crates/fabro-store/src/slate/mod.rs ++++ b/lib/crates/fabro-store/src/slate/mod.rs +@@ -17,12 +17,12 @@ use crate::{Error, ListRunsQuery, Result, RunSummary, keys}; + + #[derive(Clone)] + pub struct Database { +- object_store: Arc, +- base_prefix: String, ++ object_store: Arc, ++ base_prefix: String, + flush_interval: Duration, +- cache_path: Option, +- db: Arc>, +- active_runs: Arc>>>, ++ cache_path: Option, ++ db: Arc>, ++ active_runs: Arc>>>, + } + + impl std::fmt::Debug for Database { +diff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs +index 388e5a18..e76b2dd7 100644 +--- a/lib/crates/fabro-store/src/slate/run_store.rs ++++ b/lib/crates/fabro-store/src/slate/run_store.rs +@@ -16,7 +16,7 @@ use crate::{Error, EventEnvelope, EventPayload, Result, RunProjection, RunSummar + const DEFAULT_EVENT_TAIL_LIMIT: usize = 1024; + #[derive(Clone)] + pub struct RunDatabase { +- inner: Arc, ++ inner: Arc, + read_only: bool, + } + +@@ -30,15 +30,15 @@ impl std::fmt::Debug for RunDatabase { + } + + pub(crate) struct RunDatabaseInner { +- run_id: RunId, +- db: Db, +- event_seq: AtomicU32, +- close_lock: Mutex<()>, +- state_lock: Mutex<()>, +- projection_cache: Mutex, +- recent_events: Mutex>, ++ run_id: RunId, ++ db: Db, ++ event_seq: AtomicU32, ++ close_lock: Mutex<()>, ++ state_lock: Mutex<()>, ++ projection_cache: Mutex, ++ recent_events: Mutex>, + recent_event_limit: usize, +- event_tx: broadcast::Sender, ++ event_tx: broadcast::Sender, + } + + impl RunDatabase { +@@ -47,7 +47,7 @@ impl RunDatabase { + recover_next_seq(&db, keys::run_events_prefix(&run_id), keys::parse_event_seq).await?; + let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16)); + Ok(Self { +- inner: Arc::new(RunDatabaseInner { ++ inner: Arc::new(RunDatabaseInner { + run_id, + db, + event_seq: AtomicU32::new(event_seq), +@@ -67,7 +67,7 @@ impl RunDatabase { + recover_next_seq(&db, keys::run_events_prefix(&run_id), keys::parse_event_seq).await?; + let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16)); + Ok(Self { +- inner: Arc::new(RunDatabaseInner { ++ inner: Arc::new(RunDatabaseInner { + run_id, + db, + event_seq: AtomicU32::new(event_seq), +@@ -91,7 +91,7 @@ impl RunDatabase { + + pub(crate) fn read_only_clone(&self) -> Self { + Self { +- inner: Arc::clone(&self.inner), ++ inner: Arc::clone(&self.inner), + read_only: true, + } + } +diff --git a/lib/crates/fabro-store/src/types.rs b/lib/crates/fabro-store/src/types.rs +index 0450b5c4..fe3fae6a 100644 +--- a/lib/crates/fabro-store/src/types.rs ++++ b/lib/crates/fabro-store/src/types.rs +@@ -8,17 +8,17 @@ use crate::{Error, Result}; + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RunSummary { +- pub run_id: RunId, +- pub workflow_name: Option, +- pub workflow_slug: Option, +- pub goal: Option, +- pub labels: HashMap, +- pub host_repo_path: Option, +- pub start_time: Option>, +- pub status: Option, +- pub status_reason: Option, +- pub pending_control: Option, +- pub duration_ms: Option, ++ pub run_id: RunId, ++ pub workflow_name: Option, ++ pub workflow_slug: Option, ++ pub goal: Option, ++ pub labels: HashMap, ++ pub host_repo_path: Option, ++ pub start_time: Option>, ++ pub status: Option, ++ pub status_reason: Option, ++ pub pending_control: Option, ++ pub duration_ms: Option, + pub total_usd_micros: Option, + } + +@@ -83,7 +83,7 @@ impl TryFrom<&EventPayload> for RunEvent { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct EventEnvelope { +- pub seq: u32, ++ pub seq: u32, + #[serde(flatten)] + pub payload: EventPayload, + } +@@ -99,27 +99,27 @@ mod tests { + #[test] + fn wire_event_envelope_round_trips() { + let event = RunEvent { +- id: "evt_1".to_string(), +- ts: Utc.with_ymd_and_hms(2026, 4, 9, 12, 0, 0).unwrap(), +- run_id: fixtures::RUN_1, +- node_id: Some("code".to_string()), +- node_label: Some("Code".to_string()), +- stage_id: Some(StageId::new("code", 1)), +- parallel_group_id: None, ++ id: "evt_1".to_string(), ++ ts: Utc.with_ymd_and_hms(2026, 4, 9, 12, 0, 0).unwrap(), ++ run_id: fixtures::RUN_1, ++ node_id: Some("code".to_string()), ++ node_label: Some("Code".to_string()), ++ stage_id: Some(StageId::new("code", 1)), ++ parallel_group_id: None, + parallel_branch_id: None, +- session_id: None, +- parent_session_id: None, +- tool_call_id: None, +- actor: None, +- body: EventBody::RunCompleted(RunCompletedProps { +- duration_ms: 42, +- artifact_count: 0, +- status: "success".to_string(), +- reason: None, +- total_usd_micros: None, ++ session_id: None, ++ parent_session_id: None, ++ tool_call_id: None, ++ actor: None, ++ body: EventBody::RunCompleted(RunCompletedProps { ++ duration_ms: 42, ++ artifact_count: 0, ++ status: "success".to_string(), ++ reason: None, ++ total_usd_micros: None, + final_git_commit_sha: None, +- final_patch: None, +- billing: None, ++ final_patch: None, ++ billing: None, + }), + }; + let payload = EventPayload::new(event.to_value().unwrap(), &fixtures::RUN_1).unwrap(); +@@ -140,30 +140,30 @@ mod tests { + let group = StageId::new("review", 2); + let branch = ParallelBranchId::new(group.clone(), 3); + let event = RunEvent { +- id: "evt_2".to_string(), +- ts: Utc.with_ymd_and_hms(2026, 4, 9, 13, 0, 0).unwrap(), +- run_id: fixtures::RUN_1, +- node_id: Some("review".to_string()), +- node_label: Some("Review".to_string()), +- stage_id: Some(StageId::new("review", 2)), +- parallel_group_id: Some(group), ++ id: "evt_2".to_string(), ++ ts: Utc.with_ymd_and_hms(2026, 4, 9, 13, 0, 0).unwrap(), ++ run_id: fixtures::RUN_1, ++ node_id: Some("review".to_string()), ++ node_label: Some("Review".to_string()), ++ stage_id: Some(StageId::new("review", 2)), ++ parallel_group_id: Some(group), + parallel_branch_id: Some(branch), +- session_id: Some("ses_42".to_string()), +- parent_session_id: Some("ses_root".to_string()), +- tool_call_id: Some("tool_call_xyz".to_string()), +- actor: Some(ActorRef::agent( ++ session_id: Some("ses_42".to_string()), ++ parent_session_id: Some("ses_root".to_string()), ++ tool_call_id: Some("tool_call_xyz".to_string()), ++ actor: Some(ActorRef::agent( + Some("ses_42".to_string()), + Some("claude-sonnet".to_string()), + )), +- body: EventBody::RunCompleted(RunCompletedProps { +- duration_ms: 100, +- artifact_count: 1, +- status: "success".to_string(), +- reason: None, +- total_usd_micros: None, ++ body: EventBody::RunCompleted(RunCompletedProps { ++ duration_ms: 100, ++ artifact_count: 1, ++ status: "success".to_string(), ++ reason: None, ++ total_usd_micros: None, + final_git_commit_sha: None, +- final_patch: None, +- billing: None, ++ final_patch: None, ++ billing: None, + }), + }; + let payload = EventPayload::new(event.to_value().unwrap(), &fixtures::RUN_1).unwrap(); +diff --git a/lib/crates/fabro-telemetry/src/buffer.rs b/lib/crates/fabro-telemetry/src/buffer.rs +index 5997159e..00b7c0ae 100644 +--- a/lib/crates/fabro-telemetry/src/buffer.rs ++++ b/lib/crates/fabro-telemetry/src/buffer.rs +@@ -6,14 +6,14 @@ use crate::event::Track; + #[derive(Clone, Copy)] + pub(crate) struct BufferPolicy { + pub count_threshold: usize, +- pub time_threshold: Duration, ++ pub time_threshold: Duration, + } + + impl Default for BufferPolicy { + fn default() -> Self { + Self { + count_threshold: 20, +- time_threshold: Duration::from_secs(60), ++ time_threshold: Duration::from_secs(60), + } + } + } +@@ -70,13 +70,13 @@ mod tests { + + fn make_track(event: &str) -> Track { + Track { +- user: User::AnonymousId { ++ user: User::AnonymousId { + anonymous_id: "test".to_string(), + }, +- event: event.to_string(), ++ event: event.to_string(), + properties: json!({}), +- context: None, +- timestamp: None, ++ context: None, ++ timestamp: None, + message_id: format!("msg-{event}"), + } + } +@@ -98,7 +98,7 @@ mod tests { + &rx, + BufferPolicy { + count_threshold: 2, +- time_threshold: Duration::from_secs(60), ++ time_threshold: Duration::from_secs(60), + }, + move |tracks| { + let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); +@@ -133,7 +133,7 @@ mod tests { + &rx, + BufferPolicy { + count_threshold: 2, +- time_threshold: Duration::from_secs(60), ++ time_threshold: Duration::from_secs(60), + }, + move |_| { + *mid.lock().unwrap() = true; +@@ -168,7 +168,7 @@ mod tests { + &rx, + BufferPolicy { + count_threshold: 100, // won't trigger +- time_threshold: Duration::from_millis(50), ++ time_threshold: Duration::from_millis(50), + }, + move |tracks| { + let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); +@@ -210,7 +210,7 @@ mod tests { + &rx, + BufferPolicy { + count_threshold: 100, // won't trigger +- time_threshold: Duration::from_secs(60), ++ time_threshold: Duration::from_secs(60), + }, + move |tracks| { + let events: Vec = tracks.iter().map(|t| t.event.clone()).collect(); +diff --git a/lib/crates/fabro-telemetry/src/event.rs b/lib/crates/fabro-telemetry/src/event.rs +index 1e98cab9..dcdd0df3 100644 +--- a/lib/crates/fabro-telemetry/src/event.rs ++++ b/lib/crates/fabro-telemetry/src/event.rs +@@ -4,13 +4,13 @@ use serde_json::Value; + #[derive(Debug, Serialize, Deserialize)] + pub struct Track { + #[serde(flatten)] +- pub user: User, +- pub event: String, ++ pub user: User, ++ pub event: String, + pub properties: Value, + #[serde(skip_serializing_if = "Option::is_none")] +- pub context: Option, ++ pub context: Option, + #[serde(skip_serializing_if = "Option::is_none")] +- pub timestamp: Option, ++ pub timestamp: Option, + #[serde(rename = "messageId")] + pub message_id: String, + } +@@ -20,7 +20,7 @@ pub struct Track { + pub enum User { + Both { + #[serde(rename = "userId")] +- user_id: String, ++ user_id: String, + #[serde(rename = "anonymousId")] + anonymous_id: String, + }, +@@ -43,13 +43,13 @@ mod tests { + #[test] + fn track_serialization_anonymous() { + let track = Track { +- user: User::AnonymousId { ++ user: User::AnonymousId { + anonymous_id: "abc-123".to_string(), + }, +- event: "Command Invoked".to_string(), ++ event: "Command Invoked".to_string(), + properties: json!({"command": "run"}), +- context: Some(json!({"os": {"name": "macos"}})), +- timestamp: Some("2025-01-01T00:00:00Z".to_string()), ++ context: Some(json!({"os": {"name": "macos"}})), ++ timestamp: Some("2025-01-01T00:00:00Z".to_string()), + message_id: "msg-1".to_string(), + }; + +@@ -74,13 +74,13 @@ mod tests { + #[test] + fn track_serialization_user_id() { + let track = Track { +- user: User::UserId { ++ user: User::UserId { + user_id: "user-456".to_string(), + }, +- event: "test".to_string(), ++ event: "test".to_string(), + properties: json!({}), +- context: None, +- timestamp: None, ++ context: None, ++ timestamp: None, + message_id: "msg-2".to_string(), + }; + +@@ -97,14 +97,14 @@ mod tests { + #[test] + fn track_serialization_both() { + let track = Track { +- user: User::Both { +- user_id: "user-456".to_string(), ++ user: User::Both { ++ user_id: "user-456".to_string(), + anonymous_id: "abc-123".to_string(), + }, +- event: "test".to_string(), ++ event: "test".to_string(), + properties: json!({}), +- context: None, +- timestamp: None, ++ context: None, ++ timestamp: None, + message_id: "msg-3".to_string(), + }; + +@@ -122,13 +122,13 @@ mod tests { + #[test] + fn track_round_trip() { + let track = Track { +- user: User::AnonymousId { ++ user: User::AnonymousId { + anonymous_id: "abc".to_string(), + }, +- event: "test".to_string(), ++ event: "test".to_string(), + properties: json!({"key": "value"}), +- context: Some(json!({"app": {"name": "fabro"}})), +- timestamp: Some("2025-06-01T12:00:00Z".to_string()), ++ context: Some(json!({"app": {"name": "fabro"}})), ++ timestamp: Some("2025-06-01T12:00:00Z".to_string()), + message_id: "msg-rt".to_string(), + }; + +diff --git a/lib/crates/fabro-telemetry/src/lib.rs b/lib/crates/fabro-telemetry/src/lib.rs +index 0f2d8b82..d6500829 100644 +--- a/lib/crates/fabro-telemetry/src/lib.rs ++++ b/lib/crates/fabro-telemetry/src/lib.rs +@@ -24,11 +24,11 @@ pub enum TelemetryLevel { + } + + struct Global { +- sender: Mutex>>, ++ sender: Mutex>>, + anonymous_id: String, +- context: Value, +- level: TelemetryLevel, +- thread: Mutex>>, ++ context: Value, ++ level: TelemetryLevel, ++ thread: Mutex>>, + } + + static GLOBAL: OnceLock = OnceLock::new(); +diff --git a/lib/crates/fabro-telemetry/src/sender.rs b/lib/crates/fabro-telemetry/src/sender.rs +index 8f97db03..a1b403d8 100644 +--- a/lib/crates/fabro-telemetry/src/sender.rs ++++ b/lib/crates/fabro-telemetry/src/sender.rs +@@ -228,13 +228,13 @@ mod tests { + // SEGMENT_WRITE_KEY is not set at compile time in tests, + // so emit() should return immediately without spawning. + let track = Track { +- user: User::AnonymousId { ++ user: User::AnonymousId { + anonymous_id: "test".to_string(), + }, +- event: "test".to_string(), ++ event: "test".to_string(), + properties: json!({}), +- context: None, +- timestamp: None, ++ context: None, ++ timestamp: None, + message_id: "msg-test".to_string(), + }; + +@@ -253,13 +253,13 @@ mod tests { + #[test] + fn upload_blocking_noops_without_write_key() { + let track = Track { +- user: User::AnonymousId { ++ user: User::AnonymousId { + anonymous_id: "test".to_string(), + }, +- event: "test".to_string(), ++ event: "test".to_string(), + properties: json!({}), +- context: None, +- timestamp: None, ++ context: None, ++ timestamp: None, + message_id: "msg-test".to_string(), + }; + +diff --git a/lib/crates/fabro-template/src/lib.rs b/lib/crates/fabro-template/src/lib.rs +index 99b91743..b7590beb 100644 +--- a/lib/crates/fabro-template/src/lib.rs ++++ b/lib/crates/fabro-template/src/lib.rs +@@ -9,9 +9,9 @@ use thiserror::Error; + + #[derive(Debug, Default, Clone)] + pub struct TemplateContext { +- goal: Option, ++ goal: Option, + inputs: HashMap, +- env: Option, ++ env: Option, + } + + impl TemplateContext { +@@ -38,7 +38,7 @@ impl TemplateContext { + E: Env + Clone + Send + Sync + fmt::Debug + 'static, + { + self.env = Some(Value::from_object(EnvLookup { +- env: env.clone(), ++ env: env.clone(), + allowlist: None, + })); + self +@@ -50,7 +50,7 @@ impl TemplateContext { + E: Env + Clone + Send + Sync + fmt::Debug + 'static, + { + self.env = Some(Value::from_object(EnvLookup { +- env: env.clone(), ++ env: env.clone(), + allowlist: Some(allowlist.to_vec()), + })); + self +@@ -66,9 +66,9 @@ impl TemplateContext { + + #[derive(Debug, Clone)] + struct RenderContext { +- goal: Option, ++ goal: Option, + inputs: Value, +- env: Option, ++ env: Option, + } + + impl Object for RenderContext { +@@ -84,7 +84,7 @@ impl Object for RenderContext { + + #[derive(Debug, Clone)] + pub struct EnvLookup { +- env: E, ++ env: E, + allowlist: Option>, + } + +diff --git a/lib/crates/fabro-test/src/lib.rs b/lib/crates/fabro-test/src/lib.rs +index 3046c1c7..3e958e8c 100644 +--- a/lib/crates/fabro-test/src/lib.rs ++++ b/lib/crates/fabro-test/src/lib.rs +@@ -106,23 +106,23 @@ pub fn require_env(name: &str) -> Option { + /// shared per nextest run when `NEXTEST_RUN_ID` is present, otherwise shared + /// per test process. + pub struct TestContext { +- pub temp_dir: PathBuf, +- pub home_dir: PathBuf, +- pub storage_dir: PathBuf, +- test_case_id: String, +- test_run_id: String, +- session_root: PathBuf, +- fabro_bin: PathBuf, +- filters: Vec<(String, String)>, +- active_socket_path: PathBuf, +- isolated_server: Option, ++ pub temp_dir: PathBuf, ++ pub home_dir: PathBuf, ++ pub storage_dir: PathBuf, ++ test_case_id: String, ++ test_run_id: String, ++ session_root: PathBuf, ++ fabro_bin: PathBuf, ++ filters: Vec<(String, String)>, ++ active_socket_path: PathBuf, ++ isolated_server: Option, + managed_storage_dirs: Vec, +- _context_root: tempfile::TempDir, ++ _context_root: tempfile::TempDir, + } + + #[derive(Debug, Clone)] + struct ServerPaths { +- root: PathBuf, ++ root: PathBuf, + storage_dir: PathBuf, + socket_path: PathBuf, + config_path: PathBuf, +@@ -130,7 +130,7 @@ struct ServerPaths { + + #[derive(Debug, Clone)] + struct SessionPaths { +- root: PathBuf, ++ root: PathBuf, + server: ServerPaths, + } + +@@ -142,7 +142,7 @@ enum SessionMode { + + #[derive(Debug, Serialize)] + struct ClientMarker { +- pid: u32, ++ pid: u32, + touched_at_ms: u128, + } + +@@ -192,29 +192,37 @@ fn session_paths() -> (SessionMode, String, SessionPaths) { + if !run_id.trim().is_empty() { + let short_id = shorten_session_id(&run_id); + let root = base_dir.join(format!("n-{short_id}")); +- return (SessionMode::Nextest, run_id, SessionPaths { +- server: ServerPaths { +- root: root.clone(), +- storage_dir: root.join("storage"), +- socket_path: root.join("fabro.sock"), +- config_path: root.join("settings.toml"), ++ return ( ++ SessionMode::Nextest, ++ run_id, ++ SessionPaths { ++ server: ServerPaths { ++ root: root.clone(), ++ storage_dir: root.join("storage"), ++ socket_path: root.join("fabro.sock"), ++ config_path: root.join("settings.toml"), ++ }, ++ root, + }, +- root, +- }); ++ ); + } + } + + let process_id = format!("process-{}", current_pid()); + let root = base_dir.join(format!("p-{}", current_pid())); +- (SessionMode::Process, process_id, SessionPaths { +- server: ServerPaths { +- root: root.clone(), +- storage_dir: root.join("storage"), +- socket_path: root.join("fabro.sock"), +- config_path: root.join("settings.toml"), ++ ( ++ SessionMode::Process, ++ process_id, ++ SessionPaths { ++ server: ServerPaths { ++ root: root.clone(), ++ storage_dir: root.join("storage"), ++ socket_path: root.join("fabro.sock"), ++ config_path: root.join("settings.toml"), ++ }, ++ root, + }, +- root, +- }) ++ ) + } + + fn short_session_base_dir() -> PathBuf { +@@ -326,7 +334,7 @@ fn live_marker_count(root: &Path) -> usize { + + fn write_marker(root: &Path) { + let marker = ClientMarker { +- pid: current_pid(), ++ pid: current_pid(), + touched_at_ms: current_timestamp_ms(), + }; + let marker_path = session_marker_path(root, marker.pid); +@@ -688,7 +696,7 @@ fn test_server_stop_timeout() -> std::time::Duration { + + fn shared_server_paths(root: &Path) -> ServerPaths { + ServerPaths { +- root: root.to_path_buf(), ++ root: root.to_path_buf(), + storage_dir: root.join("storage"), + socket_path: root.join("fabro.sock"), + config_path: root.join("settings.toml"), +@@ -702,7 +710,7 @@ fn isolated_server_paths( + ) -> ServerPaths { + let server_root = root.join("isolated").join(test_case_id); + ServerPaths { +- root: server_root.clone(), ++ root: server_root.clone(), + storage_dir: storage_dir.unwrap_or_else(|| server_root.join("storage")), + socket_path: server_root.join("fabro.sock"), + config_path: server_root.join("settings.toml"), +@@ -721,7 +729,7 @@ fn reap_isolated_servers(root: &Path) { + continue; + } + stop_test_server(&ServerPaths { +- root: server_root.clone(), ++ root: server_root.clone(), + storage_dir: server_root.join("storage"), + socket_path: server_root.join("fabro.sock"), + config_path: server_root.join("settings.toml"), +@@ -1284,7 +1292,7 @@ impl Drop for TestContext { + fn drop(&mut self) { + for storage_dir in &self.managed_storage_dirs { + stop_test_server(&ServerPaths { +- root: storage_dir.clone(), ++ root: storage_dir.clone(), + storage_dir: storage_dir.clone(), + socket_path: PathBuf::new(), + config_path: PathBuf::new(), +@@ -1430,7 +1438,7 @@ pub struct TwinOpenAi { + + pub struct TwinGitHub { + pub base_url: String, +- server: twin_github::TestServer, ++ server: twin_github::TestServer, + } + + pub fn test_http_client() -> fabro_http::HttpClient { +@@ -1519,7 +1527,7 @@ impl TwinScenarios { + #[derive(Debug, Clone)] + pub struct TwinScenario { + matcher: Map, +- script: Value, ++ script: Value, + } + + impl TwinScenario { +@@ -1533,7 +1541,7 @@ impl TwinScenario { + ), + ("model".to_string(), Value::String(model.into())), + ]), +- script: json!({ "kind": "success" }), ++ script: json!({ "kind": "success" }), + } + } + +@@ -1627,7 +1635,7 @@ impl TwinScenario { + + #[derive(Debug, Clone)] + pub struct TwinToolCall { +- name: String, ++ name: String, + arguments: Value, + } + +@@ -1709,7 +1717,7 @@ pub async fn twin_openai() -> &'static TwinOpenAi { + let base_url = format!("http://127.0.0.1:{}/v1", addr.port()); + + let config = TwinConfig { +- bind_addr: addr, ++ bind_addr: addr, + require_auth: true, + enable_admin: true, + }; +@@ -1858,18 +1866,18 @@ mod tests { + fn run_and_create_commands_include_test_labels() { + let context_root = tempfile::tempdir().expect("failed to create temp dir"); + let context = TestContext { +- temp_dir: context_root.path().join("temp"), +- home_dir: context_root.path().join("home"), +- storage_dir: context_root.path().join("storage"), +- test_case_id: "case-123".to_string(), +- test_run_id: "run-cmd-labels".to_string(), +- session_root: context_root.path().join("session"), +- fabro_bin: context_root.path().join("fabro"), +- filters: Vec::new(), +- active_socket_path: context_root.path().join("fabro.sock"), +- isolated_server: None, ++ temp_dir: context_root.path().join("temp"), ++ home_dir: context_root.path().join("home"), ++ storage_dir: context_root.path().join("storage"), ++ test_case_id: "case-123".to_string(), ++ test_run_id: "run-cmd-labels".to_string(), ++ session_root: context_root.path().join("session"), ++ fabro_bin: context_root.path().join("fabro"), ++ filters: Vec::new(), ++ active_socket_path: context_root.path().join("fabro.sock"), ++ isolated_server: None, + managed_storage_dirs: Vec::new(), +- _context_root: context_root, ++ _context_root: context_root, + }; + + let run_args = context +@@ -1901,7 +1909,7 @@ mod tests { + } + + struct EnvGuard { +- key: &'static str, ++ key: &'static str, + original: Option, + } + +diff --git a/lib/crates/fabro-tracker/src/github.rs b/lib/crates/fabro-tracker/src/github.rs +index 10aa4de9..acab00aa 100644 +--- a/lib/crates/fabro-tracker/src/github.rs ++++ b/lib/crates/fabro-tracker/src/github.rs +@@ -29,12 +29,12 @@ async fn execute_github_graphql( + /// + /// Scoped to a single project board identified by `project_number`. + pub struct GitHubTracker { +- creds: GitHubCredentials, +- client: fabro_http::HttpClient, +- owner: String, +- repo: String, +- project_number: u64, +- base_url: String, ++ creds: GitHubCredentials, ++ client: fabro_http::HttpClient, ++ owner: String, ++ repo: String, ++ project_number: u64, ++ base_url: String, + project_node_id: OnceCell, + } + +@@ -632,7 +632,7 @@ mod tests { + fn mock_github_tracker(server_url: &str, pem: String) -> GitHubTracker { + GitHubTracker::new( + GitHubCredentials::App(GitHubAppCredentials { +- app_id: "test-app".to_string(), ++ app_id: "test-app".to_string(), + private_key_pem: pem, + }), + test_http_client(), +@@ -645,20 +645,20 @@ mod tests { + + fn make_test_issue(state: &str) -> Issue { + Issue { +- id: "I_issue1".to_string(), ++ id: "I_issue1".to_string(), + project_item_id: Some("PVTI_item1".to_string()), +- identifier: "#42".to_string(), +- title: "Fix bug".to_string(), +- description: None, +- priority: None, +- state: state.to_string(), +- branch_name: None, +- url: "https://github.com/owner/repo/issues/42".to_string(), +- assignee_id: None, +- labels: vec![], +- blocked_by: vec![], +- created_at: None, +- updated_at: None, ++ identifier: "#42".to_string(), ++ title: "Fix bug".to_string(), ++ description: None, ++ priority: None, ++ state: state.to_string(), ++ branch_name: None, ++ url: "https://github.com/owner/repo/issues/42".to_string(), ++ assignee_id: None, ++ labels: vec![], ++ blocked_by: vec![], ++ created_at: None, ++ updated_at: None, + } + } + +diff --git a/lib/crates/fabro-tracker/src/lib.rs b/lib/crates/fabro-tracker/src/lib.rs +index ca202caf..677e2bf6 100644 +--- a/lib/crates/fabro-tracker/src/lib.rs ++++ b/lib/crates/fabro-tracker/src/lib.rs +@@ -67,15 +67,15 @@ pub(crate) async fn execute_graphql_request( + + #[derive(Debug, Clone)] + pub struct BlockerRef { +- pub id: String, ++ pub id: String, + pub identifier: String, +- pub state: String, ++ pub state: String, + } + + #[derive(Debug, Clone)] + pub struct Issue { + /// Provider-native issue node ID. +- pub id: String, ++ pub id: String, + /// Provider-native project-item ID for status updates. + /// None for providers where the issue ID is sufficient (e.g. Linear). + /// For GitHub Projects, this is the ProjectV2Item node ID. +@@ -83,18 +83,18 @@ pub struct Issue { + /// even when an issue belongs to multiple project boards. + pub project_item_id: Option, + /// Human-readable identifier (e.g. "ABC-123" or "#42"). +- pub identifier: String, +- pub title: String, +- pub description: Option, +- pub priority: Option, +- pub state: String, +- pub branch_name: Option, +- pub url: String, +- pub assignee_id: Option, +- pub labels: Vec, +- pub blocked_by: Vec, +- pub created_at: Option, +- pub updated_at: Option, ++ pub identifier: String, ++ pub title: String, ++ pub description: Option, ++ pub priority: Option, ++ pub state: String, ++ pub branch_name: Option, ++ pub url: String, ++ pub assignee_id: Option, ++ pub labels: Vec, ++ pub blocked_by: Vec, ++ pub created_at: Option, ++ pub updated_at: Option, + } + + /// Unified interface for project management / issue tracking systems. +diff --git a/lib/crates/fabro-tracker/src/linear.rs b/lib/crates/fabro-tracker/src/linear.rs +index cbe6fa0d..d24009d0 100644 +--- a/lib/crates/fabro-tracker/src/linear.rs ++++ b/lib/crates/fabro-tracker/src/linear.rs +@@ -11,7 +11,7 @@ const BLOCKS_RELATION_TYPE: &str = "blocks"; + + #[derive(Clone, Debug)] + pub struct LinearOptions { +- pub api_key: String, ++ pub api_key: String, + pub endpoint: String, + } + +@@ -93,9 +93,9 @@ fn normalize_issue(node: &Value) -> Result { + .filter_map(|rel| { + let issue = &rel["issue"]; + Some(BlockerRef { +- id: issue["id"].as_str()?.to_string(), ++ id: issue["id"].as_str()?.to_string(), + identifier: issue["identifier"].as_str()?.to_string(), +- state: issue["state"]["name"].as_str()?.to_string(), ++ state: issue["state"]["name"].as_str()?.to_string(), + }) + }) + .collect() +@@ -154,8 +154,8 @@ fn extract_issues(response: &Value) -> Result, String> { + + /// A `Tracker` implementation backed by Linear. + pub struct LinearTracker { +- config: LinearOptions, +- client: fabro_http::HttpClient, ++ config: LinearOptions, ++ client: fabro_http::HttpClient, + project_slug: String, + } + +@@ -365,27 +365,27 @@ mod tests { + + fn mock_config(server_url: &str) -> LinearOptions { + LinearOptions { +- api_key: "lin_api_test123".to_string(), ++ api_key: "lin_api_test123".to_string(), + endpoint: format!("{server_url}/graphql"), + } + } + + fn make_test_issue() -> Issue { + Issue { +- id: "issue-1".to_string(), ++ id: "issue-1".to_string(), + project_item_id: None, +- identifier: "T-1".to_string(), +- title: "Test".to_string(), +- description: None, +- priority: None, +- state: "Todo".to_string(), +- branch_name: None, +- url: "https://linear.app/t".to_string(), +- assignee_id: None, +- labels: vec![], +- blocked_by: vec![], +- created_at: None, +- updated_at: None, ++ identifier: "T-1".to_string(), ++ title: "Test".to_string(), ++ description: None, ++ priority: None, ++ state: "Todo".to_string(), ++ branch_name: None, ++ url: "https://linear.app/t".to_string(), ++ assignee_id: None, ++ labels: vec![], ++ blocked_by: vec![], ++ created_at: None, ++ updated_at: None, + } + } + +diff --git a/lib/crates/fabro-types/src/checkpoint.rs b/lib/crates/fabro-types/src/checkpoint.rs +index 6772034c..58d5d833 100644 +--- a/lib/crates/fabro-types/src/checkpoint.rs ++++ b/lib/crates/fabro-types/src/checkpoint.rs +@@ -10,21 +10,21 @@ use crate::outcome::Outcome; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Checkpoint { +- pub timestamp: DateTime, +- pub current_node: String, +- pub completed_nodes: Vec, +- pub node_retries: HashMap, +- pub context_values: HashMap, ++ pub timestamp: DateTime, ++ pub current_node: String, ++ pub completed_nodes: Vec, ++ pub node_retries: HashMap, ++ pub context_values: HashMap, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub node_outcomes: HashMap>>, ++ pub node_outcomes: HashMap>>, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub next_node_id: Option, ++ pub next_node_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub git_commit_sha: Option, ++ pub git_commit_sha: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub loop_failure_signatures: HashMap, ++ pub loop_failure_signatures: HashMap, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub restart_failure_signatures: HashMap, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub node_visits: HashMap, ++ pub node_visits: HashMap, + } +diff --git a/lib/crates/fabro-types/src/conclusion.rs b/lib/crates/fabro-types/src/conclusion.rs +index 15fec67c..4b5e3e17 100644 +--- a/lib/crates/fabro-types/src/conclusion.rs ++++ b/lib/crates/fabro-types/src/conclusion.rs +@@ -6,27 +6,27 @@ use crate::outcome::StageStatus; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct StageSummary { +- pub stage_id: String, +- pub stage_label: String, +- pub duration_ms: u64, ++ pub stage_id: String, ++ pub stage_label: String, ++ pub duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub billing_usd_micros: Option, +- pub retries: u32, ++ pub retries: u32, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Conclusion { +- pub timestamp: DateTime, +- pub status: StageStatus, +- pub duration_ms: u64, ++ pub timestamp: DateTime, ++ pub status: StageStatus, ++ pub duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub failure_reason: Option, ++ pub failure_reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub final_git_commit_sha: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub stages: Vec, ++ pub stages: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub billing: Option, ++ pub billing: Option, + #[serde(default)] +- pub total_retries: u32, ++ pub total_retries: u32, + } +diff --git a/lib/crates/fabro-types/src/graph.rs b/lib/crates/fabro-types/src/graph.rs +index 9caa0337..6375d75e 100644 +--- a/lib/crates/fabro-types/src/graph.rs ++++ b/lib/crates/fabro-types/src/graph.rs +@@ -99,8 +99,8 @@ pub fn shape_to_handler_type(shape: &str) -> Option<&'static str> { + /// A node in the workflow graph. + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct Node { +- pub id: String, +- pub attrs: HashMap, ++ pub id: String, ++ pub attrs: HashMap, + /// CSS-like classes for model stylesheet targeting (from `class` attr and + /// subgraph derivation). + #[serde(default, skip_serializing_if = "Vec::is_empty")] +@@ -110,8 +110,8 @@ pub struct Node { + impl Node { + pub fn new(id: impl Into) -> Self { + Self { +- id: id.into(), +- attrs: HashMap::new(), ++ id: id.into(), ++ attrs: HashMap::new(), + classes: Vec::new(), + } + } +@@ -261,16 +261,16 @@ impl Node { + /// An edge connecting two nodes in the workflow graph. + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct Edge { +- pub from: String, +- pub to: String, ++ pub from: String, ++ pub to: String, + pub attrs: HashMap, + } + + impl Edge { + pub fn new(from: impl Into, to: impl Into) -> Self { + Self { +- from: from.into(), +- to: to.into(), ++ from: from.into(), ++ to: to.into(), + attrs: HashMap::new(), + } + } +@@ -327,7 +327,7 @@ impl Edge { + /// attributes. + #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] + pub struct Graph { +- pub name: String, ++ pub name: String, + pub nodes: HashMap, + pub edges: Vec, + pub attrs: HashMap, +@@ -336,7 +336,7 @@ pub struct Graph { + impl Graph { + pub fn new(name: impl Into) -> Self { + Self { +- name: name.into(), ++ name: name.into(), + nodes: HashMap::new(), + edges: Vec::new(), + attrs: HashMap::new(), +diff --git a/lib/crates/fabro-types/src/interview.rs b/lib/crates/fabro-types/src/interview.rs +index da7e3b4a..d3d42ddf 100644 +--- a/lib/crates/fabro-types/src/interview.rs ++++ b/lib/crates/fabro-types/src/interview.rs +@@ -43,17 +43,17 @@ impl fmt::Display for InterviewQuestionType { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] + pub struct InterviewQuestionRecord { + #[serde(default)] +- pub id: String, ++ pub id: String, + #[serde(default)] +- pub text: String, ++ pub text: String, + #[serde(default)] +- pub stage: String, ++ pub stage: String, + #[serde(default)] +- pub question_type: InterviewQuestionType, ++ pub question_type: InterviewQuestionType, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub options: Vec, ++ pub options: Vec, + #[serde(default)] +- pub allow_freeform: bool, ++ pub allow_freeform: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout_seconds: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +diff --git a/lib/crates/fabro-types/src/node_status.rs b/lib/crates/fabro-types/src/node_status.rs +index 00cb112b..89b31567 100644 +--- a/lib/crates/fabro-types/src/node_status.rs ++++ b/lib/crates/fabro-types/src/node_status.rs +@@ -5,10 +5,10 @@ use crate::outcome::StageStatus; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct NodeStatusRecord { +- pub status: StageStatus, ++ pub status: StageStatus, + #[serde(default)] +- pub notes: Option, ++ pub notes: Option, + #[serde(default)] + pub failure_reason: Option, +- pub timestamp: DateTime, ++ pub timestamp: DateTime, + } +diff --git a/lib/crates/fabro-types/src/outcome.rs b/lib/crates/fabro-types/src/outcome.rs +index 17680220..5660f6aa 100644 +--- a/lib/crates/fabro-types/src/outcome.rs ++++ b/lib/crates/fabro-types/src/outcome.rs +@@ -119,9 +119,9 @@ impl FailureCategory { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct FailureDetail { +- pub message: String, ++ pub message: String, + #[serde(rename = "failure_class")] +- pub category: FailureCategory, ++ pub category: FailureCategory, + #[serde( + rename = "failure_signature", + default, +@@ -143,40 +143,40 @@ impl FailureDetail { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + #[serde(bound = "M: OutcomeMeta")] + pub struct Outcome { +- pub status: StageStatus, ++ pub status: StageStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub preferred_label: Option, ++ pub preferred_label: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub suggested_next_ids: Vec, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub context_updates: HashMap, ++ pub context_updates: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub jump_to_node: Option, ++ pub jump_to_node: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub notes: Option, ++ pub notes: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub failure: Option, ++ pub failure: Option, + #[serde(default)] +- pub usage: M, ++ pub usage: M, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub files_touched: Vec, ++ pub files_touched: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub duration_ms: Option, ++ pub duration_ms: Option, + } + + impl Default for Outcome { + fn default() -> Self { + Self { +- status: StageStatus::Success, +- preferred_label: None, ++ status: StageStatus::Success, ++ preferred_label: None, + suggested_next_ids: Vec::new(), +- context_updates: HashMap::new(), +- jump_to_node: None, +- notes: None, +- failure: None, +- usage: M::default(), +- files_touched: Vec::new(), +- duration_ms: None, ++ context_updates: HashMap::new(), ++ jump_to_node: None, ++ notes: None, ++ failure: None, ++ usage: M::default(), ++ files_touched: Vec::new(), ++ duration_ms: None, + } + } + } +@@ -190,8 +190,8 @@ impl Outcome { + Self { + status: StageStatus::Fail, + failure: Some(FailureDetail { +- message: message.to_string(), +- category: FailureCategory::Deterministic, ++ message: message.to_string(), ++ category: FailureCategory::Deterministic, + signature: None, + }), + ..Self::default() +@@ -209,9 +209,9 @@ impl Outcome { + + #[derive(Debug, Clone)] + pub struct NodeResult { +- pub outcome: Outcome, +- pub duration: Duration, +- pub attempts: u32, ++ pub outcome: Outcome, ++ pub duration: Duration, ++ pub attempts: u32, + pub max_attempts: u32, + } + +diff --git a/lib/crates/fabro-types/src/pull_request.rs b/lib/crates/fabro-types/src/pull_request.rs +index 8eb66b86..c1170abd 100644 +--- a/lib/crates/fabro-types/src/pull_request.rs ++++ b/lib/crates/fabro-types/src/pull_request.rs +@@ -3,11 +3,11 @@ use serde::{Deserialize, Serialize}; + /// Record of a pull request created for a workflow run. + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct PullRequestRecord { +- pub html_url: String, +- pub number: u64, +- pub owner: String, +- pub repo: String, ++ pub html_url: String, ++ pub number: u64, ++ pub owner: String, ++ pub repo: String, + pub base_branch: String, + pub head_branch: String, +- pub title: String, ++ pub title: String, + } +diff --git a/lib/crates/fabro-types/src/retro.rs b/lib/crates/fabro-types/src/retro.rs +index 8b1f02bf..a43f99ce 100644 +--- a/lib/crates/fabro-types/src/retro.rs ++++ b/lib/crates/fabro-types/src/retro.rs +@@ -40,7 +40,7 @@ pub enum LearningCategory { + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Learning { + pub category: LearningCategory, +- pub text: String, ++ pub text: String, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +@@ -55,10 +55,10 @@ pub enum FrictionKind { + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct FrictionPoint { +- pub kind: FrictionKind, ++ pub kind: FrictionKind, + pub description: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub stage_id: Option, ++ pub stage_id: Option, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +@@ -72,72 +72,72 @@ pub enum OpenItemKind { + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct OpenItem { +- pub kind: OpenItemKind, ++ pub kind: OpenItemKind, + pub description: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct StageRetro { +- pub stage_id: String, +- pub stage_label: String, +- pub status: String, +- pub duration_ms: u64, +- pub retries: u32, ++ pub stage_id: String, ++ pub stage_label: String, ++ pub status: String, ++ pub duration_ms: u64, ++ pub retries: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub billing_usd_micros: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub notes: Option, ++ pub notes: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub failure_reason: Option, ++ pub failure_reason: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub files_touched: Vec, ++ pub files_touched: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct AggregateStats { +- pub total_duration_ms: u64, ++ pub total_duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_billing_usd_micros: Option, +- pub total_retries: u32, ++ pub total_retries: u32, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub files_touched: Vec, +- pub stages_completed: usize, +- pub stages_failed: usize, ++ pub files_touched: Vec, ++ pub stages_completed: usize, ++ pub stages_failed: usize, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct RetroNarrative { +- pub smoothness: SmoothnessRating, +- pub intent: String, +- pub outcome: String, ++ pub smoothness: SmoothnessRating, ++ pub intent: String, ++ pub outcome: String, + #[serde(default)] +- pub learnings: Vec, ++ pub learnings: Vec, + #[serde(default)] + pub friction_points: Vec, + #[serde(default)] +- pub open_items: Vec, ++ pub open_items: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Retro { +- pub run_id: RunId, +- pub workflow_name: String, +- pub goal: String, +- pub timestamp: DateTime, ++ pub run_id: RunId, ++ pub workflow_name: String, ++ pub goal: String, ++ pub timestamp: DateTime, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub smoothness: Option, +- pub stages: Vec, +- pub stats: AggregateStats, ++ pub smoothness: Option, ++ pub stages: Vec, ++ pub stats: AggregateStats, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub intent: Option, ++ pub intent: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub outcome: Option, ++ pub outcome: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub learnings: Option>, ++ pub learnings: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub friction_points: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub open_items: Option>, ++ pub open_items: Option>, + } + + impl Retro { +diff --git a/lib/crates/fabro-types/src/run.rs b/lib/crates/fabro-types/src/run.rs +index ec9fcd6b..ec014389 100644 +--- a/lib/crates/fabro-types/src/run.rs ++++ b/lib/crates/fabro-types/src/run.rs +@@ -26,48 +26,48 @@ pub struct RunClientProvenance { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_agent: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, ++ pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub version: Option, ++ pub version: Option, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct RunSubjectProvenance { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub login: Option, ++ pub login: Option, + pub auth_method: RunAuthMethod, + } + + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] + pub struct RunProvenance { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub server: Option, ++ pub server: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub client: Option, ++ pub client: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subject: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct RunRecord { +- pub run_id: RunId, +- pub settings: SettingsLayer, +- pub graph: Graph, ++ pub run_id: RunId, ++ pub settings: SettingsLayer, ++ pub graph: Graph, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub workflow_slug: Option, ++ pub workflow_slug: Option, + pub working_directory: PathBuf, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub host_repo_path: Option, ++ pub host_repo_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub repo_origin_url: Option, ++ pub repo_origin_url: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub base_branch: Option, ++ pub base_branch: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub labels: HashMap, ++ pub labels: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provenance: Option, ++ pub provenance: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub manifest_blob: Option, ++ pub manifest_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub definition_blob: Option, ++ pub definition_blob: Option, + } +diff --git a/lib/crates/fabro-types/src/run_event/agent.rs b/lib/crates/fabro-types/src/run_event/agent.rs +index 475dee84..2ea2c3ba 100644 +--- a/lib/crates/fabro-types/src/run_event/agent.rs ++++ b/lib/crates/fabro-types/src/run_event/agent.rs +@@ -8,8 +8,8 @@ pub struct AgentSessionStartedProps { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub model: Option, +- pub visit: u32, ++ pub model: Option, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -24,34 +24,34 @@ pub struct AgentProcessingEndProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentInputProps { +- pub text: String, ++ pub text: String, + pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentMessageProps { +- pub text: String, +- pub model: String, +- pub billing: BilledTokenCounts, ++ pub text: String, ++ pub model: String, ++ pub billing: BilledTokenCounts, + pub tool_call_count: usize, +- pub visit: u32, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentToolStartedProps { +- pub tool_name: String, ++ pub tool_name: String, + pub tool_call_id: String, +- pub arguments: Value, +- pub visit: u32, ++ pub arguments: Value, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentToolCompletedProps { +- pub tool_name: String, ++ pub tool_name: String, + pub tool_call_id: String, +- pub output: Value, +- pub is_error: bool, +- pub visit: u32, ++ pub output: Value, ++ pub is_error: bool, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -62,10 +62,10 @@ pub struct AgentErrorProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentWarningProps { +- pub kind: String, ++ pub kind: String, + pub message: String, + pub details: Value, +- pub visit: u32, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -76,83 +76,83 @@ pub struct AgentLoopDetectedProps { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentTurnLimitReachedProps { + pub max_turns: usize, +- pub visit: u32, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentSteeringInjectedProps { +- pub text: String, ++ pub text: String, + pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentCompactionStartedProps { +- pub estimated_tokens: usize, ++ pub estimated_tokens: usize, + pub context_window_size: usize, +- pub visit: u32, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentCompactionCompletedProps { +- pub original_turn_count: usize, +- pub preserved_turn_count: usize, ++ pub original_turn_count: usize, ++ pub preserved_turn_count: usize, + pub summary_token_estimate: usize, +- pub tracked_file_count: usize, +- pub visit: u32, ++ pub tracked_file_count: usize, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentLlmRetryProps { +- pub provider: String, +- pub model: String, +- pub attempt: usize, ++ pub provider: String, ++ pub model: String, ++ pub attempt: usize, + pub delay_secs: f64, +- pub error: Value, +- pub visit: u32, ++ pub error: Value, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentSubSpawnedProps { + pub agent_id: String, +- pub depth: usize, +- pub task: String, +- pub visit: u32, ++ pub depth: usize, ++ pub task: String, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentSubCompletedProps { +- pub agent_id: String, +- pub depth: usize, +- pub success: bool, ++ pub agent_id: String, ++ pub depth: usize, ++ pub success: bool, + pub turns_used: usize, +- pub visit: u32, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentSubFailedProps { + pub agent_id: String, +- pub depth: usize, +- pub error: Value, +- pub visit: u32, ++ pub depth: usize, ++ pub error: Value, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentSubClosedProps { + pub agent_id: String, +- pub depth: usize, +- pub visit: u32, ++ pub depth: usize, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentMcpReadyProps { + pub server_name: String, +- pub tool_count: usize, +- pub visit: u32, ++ pub tool_count: usize, ++ pub visit: u32, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentMcpFailedProps { + pub server_name: String, +- pub error: String, +- pub visit: u32, ++ pub error: String, ++ pub visit: u32, + } +diff --git a/lib/crates/fabro-types/src/run_event/infra.rs b/lib/crates/fabro-types/src/run_event/infra.rs +index 4460c0c2..5d46e403 100644 +--- a/lib/crates/fabro-types/src/run_event/infra.rs ++++ b/lib/crates/fabro-types/src/run_event/infra.rs +@@ -7,22 +7,22 @@ pub struct SandboxInitializingProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SandboxReadyProps { +- pub provider: String, ++ pub provider: String, + pub duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, ++ pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub cpu: Option, ++ pub cpu: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub memory: Option, ++ pub memory: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub url: Option, ++ pub url: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SandboxFailedProps { +- pub provider: String, +- pub error: String, ++ pub provider: String, ++ pub error: String, + pub duration_ms: u64, + } + +@@ -33,14 +33,14 @@ pub struct SandboxCleanupStartedProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SandboxCleanupCompletedProps { +- pub provider: String, ++ pub provider: String, + pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SandboxCleanupFailedProps { + pub provider: String, +- pub error: String, ++ pub error: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -50,45 +50,45 @@ pub struct SnapshotNameProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SnapshotCompletedProps { +- pub name: String, ++ pub name: String, + pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SnapshotFailedProps { +- pub name: String, ++ pub name: String, + pub error: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct GitCloneStartedProps { +- pub url: String, ++ pub url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub branch: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct GitCloneCompletedProps { +- pub url: String, ++ pub url: String, + pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct GitCloneFailedProps { +- pub url: String, ++ pub url: String, + pub error: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SandboxInitializedProps { +- pub working_directory: String, +- pub provider: String, ++ pub working_directory: String, ++ pub provider: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub identifier: Option, ++ pub identifier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub host_working_directory: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub container_mount_point: Option, ++ pub container_mount_point: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -99,14 +99,14 @@ pub struct SetupStartedProps { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SetupCommandStartedProps { + pub command: String, +- pub index: usize, ++ pub index: usize, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SetupCommandCompletedProps { +- pub command: String, +- pub index: usize, +- pub exit_code: i32, ++ pub command: String, ++ pub index: usize, ++ pub exit_code: i32, + pub duration_ms: u64, + } + +@@ -117,10 +117,10 @@ pub struct SetupCompletedProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SetupFailedProps { +- pub command: String, +- pub index: usize, ++ pub command: String, ++ pub index: usize, + pub exit_code: i32, +- pub stderr: String, ++ pub stderr: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -131,62 +131,62 @@ pub struct CliEnsureStartedProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct CliEnsureCompletedProps { +- pub cli_name: String, +- pub provider: String, ++ pub cli_name: String, ++ pub provider: String, + pub already_installed: bool, +- pub node_installed: bool, +- pub duration_ms: u64, ++ pub node_installed: bool, ++ pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct CliEnsureFailedProps { +- pub cli_name: String, +- pub provider: String, +- pub error: String, ++ pub cli_name: String, ++ pub provider: String, ++ pub error: String, + pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct DevcontainerResolvedProps { +- pub dockerfile_lines: usize, +- pub environment_count: usize, ++ pub dockerfile_lines: usize, ++ pub environment_count: usize, + pub lifecycle_command_count: usize, +- pub workspace_folder: String, ++ pub workspace_folder: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct DevcontainerLifecycleStartedProps { +- pub phase: String, ++ pub phase: String, + pub command_count: usize, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct DevcontainerLifecycleCommandStartedProps { +- pub phase: String, ++ pub phase: String, + pub command: String, +- pub index: usize, ++ pub index: usize, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct DevcontainerLifecycleCommandCompletedProps { +- pub phase: String, +- pub command: String, +- pub index: usize, +- pub exit_code: i32, ++ pub phase: String, ++ pub command: String, ++ pub index: usize, ++ pub exit_code: i32, + pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct DevcontainerLifecycleCompletedProps { +- pub phase: String, ++ pub phase: String, + pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct DevcontainerLifecycleFailedProps { +- pub phase: String, +- pub command: String, +- pub index: usize, ++ pub phase: String, ++ pub command: String, ++ pub index: usize, + pub exit_code: i32, +- pub stderr: String, ++ pub stderr: String, + } +diff --git a/lib/crates/fabro-types/src/run_event/misc.rs b/lib/crates/fabro-types/src/run_event/misc.rs +index 7a3c4d30..b12aa597 100644 +--- a/lib/crates/fabro-types/src/run_event/misc.rs ++++ b/lib/crates/fabro-types/src/run_event/misc.rs +@@ -3,15 +3,15 @@ use serde_json::Value; + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct InterviewOption { +- pub key: String, ++ pub key: String, + pub label: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct ParallelStartedProps { +- pub visit: u32, ++ pub visit: u32, + pub branch_count: usize, +- pub join_policy: String, ++ pub join_policy: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -21,35 +21,35 @@ pub struct ParallelBranchStartedProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct ParallelBranchCompletedProps { +- pub index: usize, ++ pub index: usize, + pub duration_ms: u64, +- pub status: String, ++ pub status: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub head_sha: Option, ++ pub head_sha: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct ParallelCompletedProps { +- pub visit: u32, +- pub duration_ms: u64, ++ pub visit: u32, ++ pub duration_ms: u64, + pub success_count: usize, + pub failure_count: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub results: Vec, ++ pub results: Vec, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct InterviewStartedProps { + #[serde(default)] +- pub question_id: String, +- pub question: String, ++ pub question_id: String, ++ pub question: String, + #[serde(default)] +- pub stage: String, +- pub question_type: String, ++ pub stage: String, ++ pub question_type: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub options: Vec, ++ pub options: Vec, + #[serde(default)] +- pub allow_freeform: bool, ++ pub allow_freeform: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout_seconds: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +@@ -60,8 +60,8 @@ pub struct InterviewStartedProps { + pub struct InterviewCompletedProps { + #[serde(default)] + pub question_id: String, +- pub question: String, +- pub answer: String, ++ pub question: String, ++ pub answer: String, + pub duration_ms: u64, + } + +@@ -69,9 +69,9 @@ pub struct InterviewCompletedProps { + pub struct InterviewTimeoutProps { + #[serde(default)] + pub question_id: String, +- pub question: String, ++ pub question: String, + #[serde(default)] +- pub stage: String, ++ pub stage: String, + pub duration_ms: u64, + } + +@@ -79,10 +79,10 @@ pub struct InterviewTimeoutProps { + pub struct InterviewInterruptedProps { + #[serde(default)] + pub question_id: String, +- pub question: String, ++ pub question: String, + #[serde(default)] +- pub stage: String, +- pub reason: String, ++ pub stage: String, ++ pub reason: String, + pub duration_ms: u64, + } + +@@ -93,19 +93,19 @@ pub struct GitCommitProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct GitPushProps { +- pub branch: String, ++ pub branch: String, + pub success: bool, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct GitBranchProps { + pub branch: String, +- pub sha: String, ++ pub sha: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct GitWorktreeAddProps { +- pub path: String, ++ pub path: String, + pub branch: String, + } + +@@ -116,7 +116,7 @@ pub struct GitWorktreeRemoveProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct GitFetchProps { +- pub branch: String, ++ pub branch: String, + pub success: bool, + } + +@@ -127,25 +127,25 @@ pub struct GitResetProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct EdgeSelectedProps { +- pub from_node: String, +- pub to_node: String, ++ pub from_node: String, ++ pub to_node: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub label: Option, ++ pub label: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub condition: Option, +- pub reason: String, ++ pub condition: Option, ++ pub reason: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub preferred_label: Option, ++ pub preferred_label: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub suggested_next_ids: Vec, +- pub stage_status: String, +- pub is_jump: bool, ++ pub stage_status: String, ++ pub is_jump: bool, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct LoopRestartProps { + pub from_node: String, +- pub to_node: String, ++ pub to_node: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -156,8 +156,8 @@ pub struct SubgraphStartedProps { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct SubgraphCompletedProps { + pub steps_executed: usize, +- pub status: String, +- pub duration_ms: u64, ++ pub status: String, ++ pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -167,13 +167,13 @@ pub struct StallWatchdogTimeoutProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct ArtifactCapturedProps { +- pub attempt: u32, +- pub node_slug: String, +- pub path: String, +- pub mime: String, +- pub content_md5: String, ++ pub attempt: u32, ++ pub node_slug: String, ++ pub path: String, ++ pub mime: String, ++ pub content_md5: String, + pub content_sha256: String, +- pub bytes: u64, ++ pub bytes: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -184,58 +184,58 @@ pub struct SshAccessReadyProps { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct FailoverProps { + pub from_provider: String, +- pub from_model: String, +- pub to_provider: String, +- pub to_model: String, +- pub error: String, ++ pub from_model: String, ++ pub to_provider: String, ++ pub to_model: String, ++ pub error: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct CommandStartedProps { +- pub script: String, +- pub command: String, +- pub language: String, ++ pub script: String, ++ pub command: String, ++ pub language: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout_ms: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct CommandCompletedProps { +- pub stdout: String, +- pub stderr: String, ++ pub stdout: String, ++ pub stderr: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub exit_code: Option, ++ pub exit_code: Option, + pub duration_ms: u64, +- pub timed_out: bool, ++ pub timed_out: bool, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentCliStartedProps { +- pub visit: u32, +- pub mode: String, ++ pub visit: u32, ++ pub mode: String, + pub provider: String, +- pub model: String, +- pub command: String, ++ pub model: String, ++ pub command: String, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct AgentCliCompletedProps { +- pub stdout: String, +- pub stderr: String, +- pub exit_code: i32, ++ pub stdout: String, ++ pub stderr: String, ++ pub exit_code: i32, + pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct PullRequestCreatedProps { +- pub pr_url: String, +- pub pr_number: u64, +- pub owner: String, +- pub repo: String, ++ pub pr_url: String, ++ pub pr_number: u64, ++ pub owner: String, ++ pub repo: String, + pub base_branch: String, + pub head_branch: String, +- pub title: String, +- pub draft: bool, ++ pub title: String, ++ pub draft: bool, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -246,24 +246,24 @@ pub struct PullRequestFailedProps { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RetroStartedProps { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub prompt: Option, ++ pub prompt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub model: Option, ++ pub model: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RetroCompletedProps { + pub duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub response: Option, ++ pub response: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub retro: Option, ++ pub retro: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RetroFailedProps { +- pub error: String, ++ pub error: String, + pub duration_ms: u64, + } +diff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs +index 6a5706af..1496563e 100644 +--- a/lib/crates/fabro-types/src/run_event/mod.rs ++++ b/lib/crates/fabro-types/src/run_event/mod.rs +@@ -36,9 +36,9 @@ pub enum ActorKind { + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct ActorRef { +- pub kind: ActorKind, ++ pub kind: ActorKind, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub id: Option, ++ pub id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display: Option, + } +@@ -47,8 +47,8 @@ impl ActorRef { + #[must_use] + pub fn user(login: String) -> Self { + Self { +- kind: ActorKind::User, +- id: Some(login.clone()), ++ kind: ActorKind::User, ++ id: Some(login.clone()), + display: Some(login), + } + } +@@ -65,19 +65,19 @@ impl ActorRef { + + #[derive(Debug, Clone, PartialEq)] + pub struct RunEvent { +- pub id: String, +- pub ts: DateTime, +- pub run_id: RunId, +- pub node_id: Option, +- pub node_label: Option, +- pub stage_id: Option, +- pub parallel_group_id: Option, ++ pub id: String, ++ pub ts: DateTime, ++ pub run_id: RunId, ++ pub node_id: Option, ++ pub node_label: Option, ++ pub stage_id: Option, ++ pub parallel_group_id: Option, + pub parallel_branch_id: Option, +- pub session_id: Option, +- pub parent_session_id: Option, +- pub tool_call_id: Option, +- pub actor: Option, +- pub body: EventBody, ++ pub session_id: Option, ++ pub parent_session_id: Option, ++ pub tool_call_id: Option, ++ pub actor: Option, ++ pub body: EventBody, + } + + #[allow(clippy::large_enum_variant)] +@@ -297,37 +297,37 @@ pub enum EventBody { + #[serde(rename = "retro.failed")] + RetroFailed(RetroFailedProps), + Unknown { +- name: String, ++ name: String, + properties: Value, + }, + } + + #[derive(Debug, Clone, Deserialize)] + struct RunEventRaw { +- id: String, +- ts: DateTime, +- run_id: RunId, ++ id: String, ++ ts: DateTime, ++ run_id: RunId, + #[serde(default)] +- node_id: Option, ++ node_id: Option, + #[serde(default)] +- node_label: Option, ++ node_label: Option, + #[serde(default)] +- stage_id: Option, ++ stage_id: Option, + #[serde(default)] +- parallel_group_id: Option, ++ parallel_group_id: Option, + #[serde(default)] + parallel_branch_id: Option, + #[serde(default)] +- session_id: Option, ++ session_id: Option, + #[serde(default)] +- parent_session_id: Option, ++ parent_session_id: Option, + #[serde(default)] +- tool_call_id: Option, ++ tool_call_id: Option, + #[serde(default)] +- actor: Option, +- event: String, ++ actor: Option, ++ event: String, + #[serde(default = "default_properties")] +- properties: Value, ++ properties: Value, + } + + fn default_properties() -> Value { +@@ -335,20 +335,20 @@ fn default_properties() -> Value { + } + + struct RunEventParts<'a> { +- id: String, +- ts: DateTime, +- run_id: RunId, +- node_id: Option, +- node_label: Option, +- stage_id: Option, +- parallel_group_id: Option, ++ id: String, ++ ts: DateTime, ++ run_id: RunId, ++ node_id: Option, ++ node_label: Option, ++ stage_id: Option, ++ parallel_group_id: Option, + parallel_branch_id: Option, +- session_id: Option, +- parent_session_id: Option, +- tool_call_id: Option, +- actor: Option, +- event: &'a str, +- properties: &'a Value, ++ session_id: Option, ++ parent_session_id: Option, ++ tool_call_id: Option, ++ actor: Option, ++ event: &'a str, ++ properties: &'a Value, + } + + impl EventBody { +@@ -593,20 +593,20 @@ impl RunEvent { + pub fn from_value(value: Value) -> serde_json::Result { + let raw: RunEventRaw = serde_json::from_value(value)?; + Self::from_parts(RunEventParts { +- id: raw.id, +- ts: raw.ts, +- run_id: raw.run_id, +- node_id: raw.node_id, +- node_label: raw.node_label, +- stage_id: raw.stage_id, +- parallel_group_id: raw.parallel_group_id, ++ id: raw.id, ++ ts: raw.ts, ++ run_id: raw.run_id, ++ node_id: raw.node_id, ++ node_label: raw.node_label, ++ stage_id: raw.stage_id, ++ parallel_group_id: raw.parallel_group_id, + parallel_branch_id: raw.parallel_branch_id, +- session_id: raw.session_id, +- parent_session_id: raw.parent_session_id, +- tool_call_id: raw.tool_call_id, +- actor: raw.actor, +- event: &raw.event, +- properties: &raw.properties, ++ session_id: raw.session_id, ++ parent_session_id: raw.parent_session_id, ++ tool_call_id: raw.tool_call_id, ++ actor: raw.actor, ++ event: &raw.event, ++ properties: &raw.properties, + }) + } + +@@ -670,7 +670,7 @@ impl RunEvent { + Ok(body) => body, + Err(err) if is_known_event_name(parts.event) => return Err(err), + Err(_) => EventBody::Unknown { +- name: parts.event.to_string(), ++ name: parts.event.to_string(), + properties: parts.properties.clone(), + }, + }; +@@ -783,21 +783,21 @@ mod tests { + #[test] + fn run_event_round_trips_json() { + let event = RunEvent { +- id: "evt_1".to_string(), +- ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z") ++ id: "evt_1".to_string(), ++ ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z") + .unwrap() + .with_timezone(&Utc), +- run_id: fixtures::RUN_1, +- node_id: Some("build".to_string()), +- node_label: Some("Build".to_string()), +- stage_id: None, +- parallel_group_id: None, ++ run_id: fixtures::RUN_1, ++ node_id: Some("build".to_string()), ++ node_label: Some("Build".to_string()), ++ stage_id: None, ++ parallel_group_id: None, + parallel_branch_id: None, +- session_id: None, +- parent_session_id: None, +- tool_call_id: None, +- actor: None, +- body: EventBody::StageCompleted(StageCompletedProps { ++ session_id: None, ++ parent_session_id: None, ++ tool_call_id: None, ++ actor: None, ++ body: EventBody::StageCompleted(StageCompletedProps { + index: 1, + duration_ms: 1234, + status: crate::StageStatus::Success, +@@ -829,15 +829,18 @@ mod tests { + fn run_event_deserializes_adjacent_layout() { + let settings = SettingsLayer::default(); + let graph = Graph { +- name: "test".to_string(), +- nodes: HashMap::from([("start".to_string(), Node { +- id: "start".to_string(), +- attrs: HashMap::new(), +- classes: Vec::new(), +- })]), ++ name: "test".to_string(), ++ nodes: HashMap::from([( ++ "start".to_string(), ++ Node { ++ id: "start".to_string(), ++ attrs: HashMap::new(), ++ classes: Vec::new(), ++ }, ++ )]), + edges: vec![Edge { +- from: "start".to_string(), +- to: "done".to_string(), ++ from: "start".to_string(), ++ to: "done".to_string(), + attrs: HashMap::new(), + }], + attrs: HashMap::new(), +@@ -891,9 +894,9 @@ mod tests { + fn interview_interrupted_kind_matches_event_name() { + let body = EventBody::InterviewInterrupted(InterviewInterruptedProps { + question_id: "q-1".to_string(), +- question: "approve?".to_string(), +- stage: "gate".to_string(), +- reason: "interrupted".to_string(), ++ question: "approve?".to_string(), ++ stage: "gate".to_string(), ++ reason: "interrupted".to_string(), + duration_ms: 12, + }); + +@@ -1025,27 +1028,27 @@ mod tests { + #[test] + fn run_event_omits_absent_envelope_fields() { + let event = RunEvent { +- id: "evt_bare".to_string(), +- ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z") ++ id: "evt_bare".to_string(), ++ ts: DateTime::parse_from_rfc3339("2026-04-04T12:00:00.000Z") + .unwrap() + .with_timezone(&Utc), +- run_id: fixtures::RUN_1, +- node_id: None, +- node_label: None, +- stage_id: None, +- parallel_group_id: None, ++ run_id: fixtures::RUN_1, ++ node_id: None, ++ node_label: None, ++ stage_id: None, ++ parallel_group_id: None, + parallel_branch_id: None, +- session_id: None, +- parent_session_id: None, +- tool_call_id: None, +- actor: None, +- body: EventBody::RunStarted(RunStartedProps { +- name: "demo".to_string(), +- base_branch: None, +- base_sha: None, +- run_branch: None, ++ session_id: None, ++ parent_session_id: None, ++ tool_call_id: None, ++ actor: None, ++ body: EventBody::RunStarted(RunStartedProps { ++ name: "demo".to_string(), ++ base_branch: None, ++ base_sha: None, ++ run_branch: None, + worktree_dir: None, +- goal: None, ++ goal: None, + }), + }; + +diff --git a/lib/crates/fabro-types/src/run_event/run.rs b/lib/crates/fabro-types/src/run_event/run.rs +index 2b5488b4..90fdb2fd 100644 +--- a/lib/crates/fabro-types/src/run_event/run.rs ++++ b/lib/crates/fabro-types/src/run_event/run.rs +@@ -8,45 +8,45 @@ use crate::{Graph, RunBlobId, RunControlAction, RunProvenance, StatusReason}; + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RunCreatedProps { +- pub settings: SettingsLayer, +- pub graph: Graph, ++ pub settings: SettingsLayer, ++ pub graph: Graph, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub workflow_source: Option, ++ pub workflow_source: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub workflow_config: Option, ++ pub workflow_config: Option, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] +- pub labels: BTreeMap, +- pub run_dir: String, ++ pub labels: BTreeMap, ++ pub run_dir: String, + pub working_directory: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub host_repo_path: Option, ++ pub host_repo_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub repo_origin_url: Option, ++ pub repo_origin_url: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub base_branch: Option, ++ pub base_branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub workflow_slug: Option, ++ pub workflow_slug: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub db_prefix: Option, ++ pub db_prefix: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provenance: Option, ++ pub provenance: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub manifest_blob: Option, ++ pub manifest_blob: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RunStartedProps { +- pub name: String, ++ pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub base_branch: Option, ++ pub base_branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub base_sha: Option, ++ pub base_sha: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub run_branch: Option, ++ pub run_branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worktree_dir: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub goal: Option, ++ pub goal: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +@@ -58,7 +58,7 @@ pub struct RunStatusTransitionProps { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RunSubmittedProps { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub reason: Option, ++ pub reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub definition_blob: Option, + } +@@ -75,44 +75,44 @@ pub struct RunControlEffectProps {} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RunRewoundProps { + pub target_checkpoint_ordinal: usize, +- pub target_node_id: String, +- pub target_visit: usize, ++ pub target_node_id: String, ++ pub target_visit: usize, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub previous_status: Option, ++ pub previous_status: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub run_commit_sha: Option, ++ pub run_commit_sha: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RunCompletedProps { +- pub duration_ms: u64, +- pub artifact_count: usize, +- pub status: String, ++ pub duration_ms: u64, ++ pub artifact_count: usize, ++ pub status: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub reason: Option, ++ pub reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub total_usd_micros: Option, ++ pub total_usd_micros: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub final_git_commit_sha: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub final_patch: Option, ++ pub final_patch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub billing: Option, ++ pub billing: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RunFailedProps { +- pub error: String, +- pub duration_ms: u64, ++ pub error: String, ++ pub duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub reason: Option, ++ pub reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub git_commit_sha: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct RunNoticeProps { +- pub level: RunNoticeLevel, +- pub code: String, ++ pub level: RunNoticeLevel, ++ pub code: String, + pub message: String, + } +diff --git a/lib/crates/fabro-types/src/run_event/stage.rs b/lib/crates/fabro-types/src/run_event/stage.rs +index e115da76..062a93fb 100644 +--- a/lib/crates/fabro-types/src/run_event/stage.rs ++++ b/lib/crates/fabro-types/src/run_event/stage.rs +@@ -7,9 +7,9 @@ use crate::{BilledModelUsage, FailureDetail, Outcome, StageStatus}; + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct StageStartedProps { +- pub index: usize, ++ pub index: usize, + pub handler_type: String, +- pub attempt: usize, ++ pub attempt: usize, + pub max_attempts: usize, + } + +@@ -50,41 +50,41 @@ pub struct StageCompletedProps { + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct StageFailedProps { +- pub index: usize, ++ pub index: usize, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub failure: Option, +- pub will_retry: bool, ++ pub failure: Option, ++ pub will_retry: bool, + #[serde(default)] + pub duration_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct StageRetryingProps { +- pub index: usize, +- pub attempt: usize, ++ pub index: usize, ++ pub attempt: usize, + pub max_attempts: usize, +- pub delay_ms: u64, ++ pub delay_ms: u64, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct StagePromptProps { +- pub visit: u32, +- pub text: String, ++ pub visit: u32, ++ pub text: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub mode: Option, ++ pub mode: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub model: Option, ++ pub model: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct PromptCompletedProps { + pub response: String, +- pub model: String, ++ pub model: String, + pub provider: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub billing: Option, ++ pub billing: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +diff --git a/lib/crates/fabro-types/src/sandbox_record.rs b/lib/crates/fabro-types/src/sandbox_record.rs +index 787d612d..59741a70 100644 +--- a/lib/crates/fabro-types/src/sandbox_record.rs ++++ b/lib/crates/fabro-types/src/sandbox_record.rs +@@ -2,12 +2,12 @@ use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct SandboxRecord { +- pub provider: String, +- pub working_directory: String, ++ pub provider: String, ++ pub working_directory: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub identifier: Option, ++ pub identifier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub host_working_directory: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub container_mount_point: Option, ++ pub container_mount_point: Option, + } +diff --git a/lib/crates/fabro-types/src/settings/cli.rs b/lib/crates/fabro-types/src/settings/cli.rs +index 177484fe..105cb723 100644 +--- a/lib/crates/fabro-types/src/settings/cli.rs ++++ b/lib/crates/fabro-types/src/settings/cli.rs +@@ -15,10 +15,10 @@ use super::run::{AgentPermissions, McpEntryLayer, McpServerSettings}; + /// A structurally resolved `[cli]` view for consumers. + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct CliSettings { +- pub target: Option, +- pub auth: CliAuthSettings, +- pub exec: CliExecSettings, +- pub output: CliOutputSettings, ++ pub target: Option, ++ pub auth: CliAuthSettings, ++ pub exec: CliExecSettings, ++ pub output: CliOutputSettings, + pub updates: CliUpdatesSettings, + pub logging: CliLoggingSettings, + } +@@ -38,8 +38,8 @@ pub enum CliTargetSettings { + #[derive(Debug, Clone, PartialEq, Serialize)] + pub struct CliTargetTlsSettings { + pub cert: InterpString, +- pub key: InterpString, +- pub ca: InterpString, ++ pub key: InterpString, ++ pub ca: InterpString, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] +@@ -50,25 +50,25 @@ pub struct CliAuthSettings { + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct CliExecSettings { + pub prevent_idle_sleep: bool, +- pub model: CliExecModelSettings, +- pub agent: CliExecAgentSettings, ++ pub model: CliExecModelSettings, ++ pub agent: CliExecAgentSettings, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct CliExecModelSettings { + pub provider: Option, +- pub name: Option, ++ pub name: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct CliExecAgentSettings { + pub permissions: Option, +- pub mcps: HashMap, ++ pub mcps: HashMap, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct CliOutputSettings { +- pub format: OutputFormat, ++ pub format: OutputFormat, + pub verbosity: OutputVerbosity, + } + +@@ -87,13 +87,13 @@ pub struct CliLoggingSettings { + #[serde(deny_unknown_fields)] + pub struct CliLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub target: Option, ++ pub target: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub auth: Option, ++ pub auth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub exec: Option, ++ pub exec: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub output: Option, ++ pub output: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub updates: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +@@ -122,9 +122,9 @@ pub struct CliTargetTlsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cert: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub key: Option, ++ pub key: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub ca: Option, ++ pub ca: Option, + } + + /// `[cli.auth]` — explicit auth strategy selection. +@@ -152,9 +152,9 @@ pub struct CliExecLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prevent_idle_sleep: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub model: Option, ++ pub model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub agent: Option, ++ pub agent: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +@@ -163,7 +163,7 @@ pub struct CliExecModelLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, ++ pub name: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +@@ -173,7 +173,7 @@ pub struct CliExecAgentLayer { + pub permissions: Option, + /// Agent-scoped MCP entries for `fabro exec`. + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub mcps: HashMap, ++ pub mcps: HashMap, + } + + /// `[cli.output]` — generic CLI output defaults. +@@ -181,7 +181,7 @@ pub struct CliExecAgentLayer { + #[serde(deny_unknown_fields)] + pub struct CliOutputLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub format: Option, ++ pub format: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub verbosity: Option, + } +diff --git a/lib/crates/fabro-types/src/settings/duration.rs b/lib/crates/fabro-types/src/settings/duration.rs +index 17d64c12..7ea3a769 100644 +--- a/lib/crates/fabro-types/src/settings/duration.rs ++++ b/lib/crates/fabro-types/src/settings/duration.rs +@@ -141,7 +141,7 @@ impl FromStr for Duration { + other => { + return Err(ParseDurationError::InvalidUnit { + input: input.to_owned(), +- unit: other.to_owned(), ++ unit: other.to_owned(), + }); + } + }; +diff --git a/lib/crates/fabro-types/src/settings/interp.rs b/lib/crates/fabro-types/src/settings/interp.rs +index fc5e2603..f893628e 100644 +--- a/lib/crates/fabro-types/src/settings/interp.rs ++++ b/lib/crates/fabro-types/src/settings/interp.rs +@@ -181,7 +181,7 @@ impl From<&str> for InterpString { + /// The outcome of a successful env interpolation resolution. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Resolved { +- pub value: String, ++ pub value: String, + pub provenance: Provenance, + } + +@@ -300,9 +300,12 @@ mod tests { + .resolve(lookup_from(&[("API_KEY", "secret-123")])) + .unwrap(); + assert_eq!(resolved.value, "secret-123"); +- assert_eq!(resolved.provenance, Provenance::EnvSourced { +- names: vec!["API_KEY".into()], +- }); ++ assert_eq!( ++ resolved.provenance, ++ Provenance::EnvSourced { ++ names: vec!["API_KEY".into()], ++ } ++ ); + } + + #[test] +@@ -319,9 +322,12 @@ mod tests { + .resolve(lookup_from(&[("USER", "root"), ("HOST", "example.com")])) + .unwrap(); + assert_eq!(resolved.value, "root@example.com"); +- assert_eq!(resolved.provenance, Provenance::EnvSourced { +- names: vec!["USER".into(), "HOST".into()], +- }); ++ assert_eq!( ++ resolved.provenance, ++ Provenance::EnvSourced { ++ names: vec!["USER".into(), "HOST".into()], ++ } ++ ); + } + + #[test] +diff --git a/lib/crates/fabro-types/src/settings/layer.rs b/lib/crates/fabro-types/src/settings/layer.rs +index 81cd94c5..6a0a2186 100644 +--- a/lib/crates/fabro-types/src/settings/layer.rs ++++ b/lib/crates/fabro-types/src/settings/layer.rs +@@ -18,17 +18,17 @@ use super::workflow::WorkflowLayer; + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] + pub struct SettingsLayer { + #[serde(default, rename = "_version", skip_serializing_if = "Option::is_none")] +- pub version: Option, ++ pub version: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub project: Option, ++ pub project: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workflow: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub run: Option, ++ pub run: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub cli: Option, ++ pub cli: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub server: Option, ++ pub server: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub features: Option, + } +diff --git a/lib/crates/fabro-types/src/settings/model_ref.rs b/lib/crates/fabro-types/src/settings/model_ref.rs +index ee912ff5..26545599 100644 +--- a/lib/crates/fabro-types/src/settings/model_ref.rs ++++ b/lib/crates/fabro-types/src/settings/model_ref.rs +@@ -81,7 +81,7 @@ impl FromStr for ModelRef { + } else { + Ok(Self::Qualified { + provider: (*provider).to_owned(), +- model: (*model).to_owned(), ++ model: (*model).to_owned(), + }) + } + } +@@ -104,9 +104,9 @@ impl fmt::Display for ModelRef { + /// An error returned when resolving an ambiguous bare model reference. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct AmbiguousModelRef { +- pub input: String, ++ pub input: String, + pub providers: Vec, +- pub models: Vec, ++ pub models: Vec, + } + + impl fmt::Display for AmbiguousModelRef { +@@ -130,7 +130,7 @@ pub enum ResolvedModelRef { + /// The reference named a model (qualified or unambiguously bare). + Model { + provider: Option, +- model: String, ++ model: String, + }, + } + +@@ -162,7 +162,7 @@ impl ModelRef { + match self { + Self::Qualified { provider, model } => Ok(ResolvedModelRef::Model { + provider: Some(provider.clone()), +- model: model.clone(), ++ model: model.clone(), + }), + Self::Bare(token) => { + let is_provider = registry.is_provider(token); +@@ -171,17 +171,17 @@ impl ModelRef { + (true, false) => Ok(ResolvedModelRef::Provider(token.clone())), + (false, true) => Ok(ResolvedModelRef::Model { + provider: registry.provider_of(token), +- model: token.clone(), ++ model: token.clone(), + }), + (true, true) => Err(AmbiguousModelRef { +- input: token.clone(), ++ input: token.clone(), + providers: vec![token.clone()], +- models: vec![token.clone()], ++ models: vec![token.clone()], + }), + // Unknown tokens flow through as bare models — provider TBD at runtime. + (false, false) => Ok(ResolvedModelRef::Model { + provider: None, +- model: token.clone(), ++ model: token.clone(), + }), + } + } +@@ -227,7 +227,7 @@ mod tests { + + struct TestRegistry { + providers: &'static [&'static str], +- models: &'static [&'static str], ++ models: &'static [&'static str], + } + + impl ModelRegistry for TestRegistry { +@@ -260,7 +260,7 @@ mod tests { + "gemini/gemini-flash".parse::().unwrap(), + ModelRef::Qualified { + provider: "gemini".into(), +- model: "gemini-flash".into(), ++ model: "gemini-flash".into(), + } + ); + } +@@ -295,7 +295,7 @@ mod tests { + fn resolves_unique_provider_token() { + let reg = TestRegistry { + providers: &["openai"], +- models: &[], ++ models: &[], + }; + let resolved = ModelRef::Bare("openai".into()).resolve(®).unwrap(); + assert_eq!(resolved, ResolvedModelRef::Provider("openai".into())); +@@ -305,20 +305,23 @@ mod tests { + fn resolves_unique_model_token() { + let reg = TestRegistry { + providers: &[], +- models: &["gpt-5.4"], ++ models: &["gpt-5.4"], + }; + let resolved = ModelRef::Bare("gpt-5.4".into()).resolve(®).unwrap(); +- assert_eq!(resolved, ResolvedModelRef::Model { +- provider: Some("test".into()), +- model: "gpt-5.4".into(), +- }); ++ assert_eq!( ++ resolved, ++ ResolvedModelRef::Model { ++ provider: Some("test".into()), ++ model: "gpt-5.4".into(), ++ } ++ ); + } + + #[test] + fn ambiguous_bare_token_errors() { + let reg = TestRegistry { + providers: &["ambiguous"], +- models: &["ambiguous"], ++ models: &["ambiguous"], + }; + let err = ModelRef::Bare("ambiguous".into()) + .resolve(®) +@@ -330,18 +333,21 @@ mod tests { + fn qualified_never_ambiguous() { + let reg = TestRegistry { + providers: &["ambiguous"], +- models: &["ambiguous"], ++ models: &["ambiguous"], + }; + let resolved = ModelRef::Qualified { + provider: "a".into(), +- model: "b".into(), ++ model: "b".into(), + } + .resolve(®) + .unwrap(); +- assert_eq!(resolved, ResolvedModelRef::Model { +- provider: Some("a".into()), +- model: "b".into(), +- }); ++ assert_eq!( ++ resolved, ++ ResolvedModelRef::Model { ++ provider: Some("a".into()), ++ model: "b".into(), ++ } ++ ); + } + + #[test] +diff --git a/lib/crates/fabro-types/src/settings/project.rs b/lib/crates/fabro-types/src/settings/project.rs +index dcdd529b..bab3575c 100644 +--- a/lib/crates/fabro-types/src/settings/project.rs ++++ b/lib/crates/fabro-types/src/settings/project.rs +@@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; + /// A structurally resolved `[project]` view for consumers. + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct ProjectSettings { +- pub name: Option, ++ pub name: Option, + pub description: Option, +- pub directory: String, +- pub metadata: HashMap, ++ pub directory: String, ++ pub metadata: HashMap, + } + + /// A sparse `[project]` layer as it appears in a single settings file. +@@ -21,13 +21,13 @@ pub struct ProjectSettings { + #[serde(deny_unknown_fields)] + pub struct ProjectLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, ++ pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The Fabro-managed project directory inside the repo. Defaults to + /// `.` after layering when unspecified. + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub directory: Option, ++ pub directory: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub metadata: HashMap, ++ pub metadata: HashMap, + } +diff --git a/lib/crates/fabro-types/src/settings/resolved.rs b/lib/crates/fabro-types/src/settings/resolved.rs +index 79a4825f..8fb582c9 100644 +--- a/lib/crates/fabro-types/src/settings/resolved.rs ++++ b/lib/crates/fabro-types/src/settings/resolved.rs +@@ -7,11 +7,11 @@ use super::{ + /// A fully resolved settings view across all namespaces. + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct Settings { +- pub project: ProjectSettings, ++ pub project: ProjectSettings, + pub workflow: WorkflowSettings, +- pub run: RunSettings, +- pub cli: CliSettings, +- pub server: ServerSettings, ++ pub run: RunSettings, ++ pub cli: CliSettings, ++ pub server: ServerSettings, + pub features: FeaturesSettings, + } + +@@ -44,8 +44,8 @@ mod tests { + url: InterpString::parse("https://api.example.com"), + tls: Some(CliTargetTlsSettings { + cert: InterpString::parse("/tmp/client.crt"), +- key: InterpString::parse("/tmp/client.key"), +- ca: InterpString::parse("/tmp/ca.pem"), ++ key: InterpString::parse("/tmp/client.key"), ++ ca: InterpString::parse("/tmp/ca.pem"), + }), + }) + .unwrap(), +@@ -71,8 +71,8 @@ mod tests { + assert_eq!( + serde_json::to_value(McpTransport::Sandbox { + command: vec!["fabro-mcp".to_string(), "--serve".to_string()], +- port: 3333, +- env: HashMap::from([("TOKEN".to_string(), "{{ env.MCP_TOKEN }}".to_string())]), ++ port: 3333, ++ env: HashMap::from([("TOKEN".to_string(), "{{ env.MCP_TOKEN }}".to_string())]), + }) + .unwrap(), + json!({ +@@ -98,9 +98,9 @@ mod tests { + + assert_eq!( + serde_json::to_value(ObjectStoreSettings::S3 { +- bucket: InterpString::parse("fabro-artifacts"), +- region: InterpString::parse("us-east-1"), +- endpoint: Some(InterpString::parse("https://s3.example.com")), ++ bucket: InterpString::parse("fabro-artifacts"), ++ region: InterpString::parse("us-east-1"), ++ endpoint: Some(InterpString::parse("https://s3.example.com")), + path_style: true, + }) + .unwrap(), +@@ -119,9 +119,9 @@ mod tests { + assert_eq!( + serde_json::to_value(ServerListenSettings::Tcp { + address: "127.0.0.1:8080".parse().unwrap(), +- tls: Some(TlsConfig { ++ tls: Some(TlsConfig { + cert: InterpString::parse("/tmp/server.crt"), +- key: InterpString::parse("/tmp/server.key"), ++ key: InterpString::parse("/tmp/server.key"), + }), + }) + .unwrap(), +@@ -138,29 +138,32 @@ mod tests { + let settings = Settings { + server: ServerSettings { + slatedb: ServerSlateDbSettings { +- prefix: InterpString::parse("slatedb/"), +- store: ObjectStoreSettings::Local { ++ prefix: InterpString::parse("slatedb/"), ++ store: ObjectStoreSettings::Local { + root: InterpString::parse("/srv/slatedb"), + }, + flush_interval: StdDuration::from_secs(30), +- disk_cache: false, ++ disk_cache: false, + }, + ..ServerSettings::default() + }, + run: RunSettings { + agent: RunAgentSettings { +- mcps: HashMap::from([("sandboxed".to_string(), McpServerSettings { +- name: "sandboxed".to_string(), +- transport: McpTransport::Http { +- url: "https://mcp.example.com".to_string(), +- headers: HashMap::from([( +- "Authorization".to_string(), +- "Bearer {{ env.MCP_TOKEN }}".to_string(), +- )]), ++ mcps: HashMap::from([( ++ "sandboxed".to_string(), ++ McpServerSettings { ++ name: "sandboxed".to_string(), ++ transport: McpTransport::Http { ++ url: "https://mcp.example.com".to_string(), ++ headers: HashMap::from([( ++ "Authorization".to_string(), ++ "Bearer {{ env.MCP_TOKEN }}".to_string(), ++ )]), ++ }, ++ startup_timeout_secs: 15, ++ tool_timeout_secs: 90, + }, +- startup_timeout_secs: 15, +- tool_timeout_secs: 90, +- })]), ++ )]), + ..RunAgentSettings::default() + }, + ..RunSettings::default() +diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs +index c5c45c13..a5eb43ce 100644 +--- a/lib/crates/fabro-types/src/settings/run.rs ++++ b/lib/crates/fabro-types/src/settings/run.rs +@@ -19,23 +19,23 @@ use super::model_ref::ModelRef; + /// A structurally resolved `[run]` view for consumers. + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct RunSettings { +- pub goal: Option, +- pub working_dir: Option, +- pub metadata: HashMap, +- pub inputs: HashMap, +- pub model: RunModelSettings, +- pub git: RunGitSettings, +- pub prepare: RunPrepareSettings, +- pub execution: RunExecutionSettings, +- pub checkpoint: RunCheckpointSettings, +- pub sandbox: RunSandboxSettings, ++ pub goal: Option, ++ pub working_dir: Option, ++ pub metadata: HashMap, ++ pub inputs: HashMap, ++ pub model: RunModelSettings, ++ pub git: RunGitSettings, ++ pub prepare: RunPrepareSettings, ++ pub execution: RunExecutionSettings, ++ pub checkpoint: RunCheckpointSettings, ++ pub sandbox: RunSandboxSettings, + pub notifications: HashMap, +- pub interviews: RunInterviewsSettings, +- pub agent: RunAgentSettings, +- pub hooks: Vec, +- pub scm: RunScmSettings, +- pub pull_request: Option, +- pub artifacts: ArtifactsSettings, ++ pub interviews: RunInterviewsSettings, ++ pub agent: RunAgentSettings, ++ pub hooks: Vec, ++ pub scm: RunScmSettings, ++ pub pull_request: Option, ++ pub artifacts: ArtifactsSettings, + } + + /// The resolved source of a run goal. +@@ -48,8 +48,8 @@ pub enum RunGoal { + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct RunModelSettings { +- pub provider: Option, +- pub name: Option, ++ pub provider: Option, ++ pub name: Option, + pub fallbacks: Vec, + } + +@@ -60,20 +60,20 @@ pub struct RunGitSettings { + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct GitAuthorSettings { +- pub name: Option, ++ pub name: Option, + pub email: Option, + } + + #[derive(Debug, Clone, PartialEq, Serialize)] + pub struct RunPrepareSettings { +- pub commands: Vec, ++ pub commands: Vec, + pub timeout_ms: u64, + } + + impl Default for RunPrepareSettings { + fn default() -> Self { + Self { +- commands: Vec::new(), ++ commands: Vec::new(), + timeout_ms: 300_000, + } + } +@@ -81,17 +81,17 @@ impl Default for RunPrepareSettings { + + #[derive(Debug, Clone, PartialEq, Serialize)] + pub struct RunExecutionSettings { +- pub mode: RunMode, ++ pub mode: RunMode, + pub approval: ApprovalMode, +- pub retros: bool, ++ pub retros: bool, + } + + impl Default for RunExecutionSettings { + fn default() -> Self { + Self { +- mode: RunMode::Normal, ++ mode: RunMode::Normal, + approval: ApprovalMode::Prompt, +- retros: true, ++ retros: true, + } + } + } +@@ -103,23 +103,23 @@ pub struct RunCheckpointSettings { + + #[derive(Debug, Clone, PartialEq, Serialize)] + pub struct RunSandboxSettings { +- pub provider: String, +- pub preserve: bool, ++ pub provider: String, ++ pub preserve: bool, + pub devcontainer: bool, +- pub env: HashMap, +- pub local: LocalSandboxSettings, +- pub daytona: Option, ++ pub env: HashMap, ++ pub local: LocalSandboxSettings, ++ pub daytona: Option, + } + + impl Default for RunSandboxSettings { + fn default() -> Self { + Self { +- provider: "local".to_string(), +- preserve: false, ++ provider: "local".to_string(), ++ preserve: false, + devcontainer: false, +- env: HashMap::new(), +- local: LocalSandboxSettings::default(), +- daytona: None, ++ env: HashMap::new(), ++ local: LocalSandboxSettings::default(), ++ daytona: None, + } + } + } +@@ -132,10 +132,10 @@ pub struct LocalSandboxSettings { + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct DaytonaSettings { + pub auto_stop_interval: Option, +- pub labels: HashMap, +- pub snapshot: Option, +- pub network: Option, +- pub skip_clone: bool, ++ pub labels: HashMap, ++ pub snapshot: Option, ++ pub network: Option, ++ pub skip_clone: bool, + } + + #[derive(Debug, Clone, PartialEq)] +@@ -166,21 +166,21 @@ impl Serialize for DockerfileSource { + + #[derive(Debug, Clone, PartialEq, Serialize)] + pub struct DaytonaSnapshotSettings { +- pub name: String, +- pub cpu: Option, +- pub memory_gb: Option, +- pub disk_gb: Option, ++ pub name: String, ++ pub cpu: Option, ++ pub memory_gb: Option, ++ pub disk_gb: Option, + pub dockerfile: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct NotificationRouteSettings { +- pub enabled: bool, ++ pub enabled: bool, + pub provider: Option, +- pub events: Vec, +- pub slack: Option, +- pub discord: Option, +- pub teams: Option, ++ pub events: Vec, ++ pub slack: Option, ++ pub discord: Option, ++ pub teams: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] +@@ -191,9 +191,9 @@ pub struct NotificationProviderSettings { + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct RunInterviewsSettings { + pub provider: Option, +- pub slack: Option, +- pub discord: Option, +- pub teams: Option, ++ pub slack: Option, ++ pub discord: Option, ++ pub teams: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] +@@ -204,27 +204,27 @@ pub struct InterviewProviderSettings { + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct RunAgentSettings { + pub permissions: Option, +- pub mcps: HashMap, ++ pub mcps: HashMap, + } + + #[derive(Debug, Clone, PartialEq, Serialize)] + pub struct McpServerSettings { +- pub name: String, +- pub transport: McpTransport, ++ pub name: String, ++ pub transport: McpTransport, + pub startup_timeout_secs: u64, +- pub tool_timeout_secs: u64, ++ pub tool_timeout_secs: u64, + } + + impl Default for McpServerSettings { + fn default() -> Self { + Self { +- name: String::new(), +- transport: McpTransport::Stdio { ++ name: String::new(), ++ transport: McpTransport::Stdio { + command: Vec::new(), +- env: HashMap::new(), ++ env: HashMap::new(), + }, + startup_timeout_secs: 10, +- tool_timeout_secs: 60, ++ tool_timeout_secs: 60, + } + } + } +@@ -246,16 +246,16 @@ impl McpServerSettings { + pub enum McpTransport { + Stdio { + command: Vec, +- env: HashMap, ++ env: HashMap, + }, + Http { +- url: String, ++ url: String, + headers: HashMap, + }, + Sandbox { + command: Vec, +- port: u16, +- env: HashMap, ++ port: u16, ++ env: HashMap, + }, + } + +@@ -275,36 +275,36 @@ pub enum HookType { + command: String, + }, + Http { +- url: String, +- headers: Option>, ++ url: String, ++ headers: Option>, + #[serde(default)] + allowed_env_vars: Vec, + #[serde(default)] +- tls: TlsMode, ++ tls: TlsMode, + }, + Prompt { + prompt: String, +- model: Option, ++ model: Option, + }, + Agent { +- prompt: String, +- model: Option, ++ prompt: String, ++ model: Option, + max_tool_rounds: Option, + }, + } + + #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] + pub struct HookDefinition { +- pub name: Option, +- pub event: HookEvent, ++ pub name: Option, ++ pub event: HookEvent, + #[serde(default)] +- pub command: Option, ++ pub command: Option, + #[serde(flatten)] +- pub hook_type: Option, +- pub matcher: Option, +- pub blocking: Option, ++ pub hook_type: Option, ++ pub matcher: Option, ++ pub blocking: Option, + pub timeout_ms: Option, +- pub sandbox: Option, ++ pub sandbox: Option, + } + + impl HookDefinition { +@@ -373,10 +373,10 @@ impl HookDefinition { + + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct RunScmSettings { +- pub provider: Option, +- pub owner: Option, ++ pub provider: Option, ++ pub owner: Option, + pub repository: Option, +- pub github: Option, ++ pub github: Option, + } + + #[derive(Debug, Clone, Default, PartialEq)] +@@ -394,18 +394,18 @@ impl Serialize for ScmGitHubSettings { + + #[derive(Debug, Clone, PartialEq, Serialize)] + pub struct PullRequestSettings { +- pub enabled: bool, +- pub draft: bool, +- pub auto_merge: bool, ++ pub enabled: bool, ++ pub draft: bool, ++ pub auto_merge: bool, + pub merge_strategy: MergeStrategy, + } + + impl Default for PullRequestSettings { + fn default() -> Self { + Self { +- enabled: false, +- draft: true, +- auto_merge: false, ++ enabled: false, ++ draft: true, ++ auto_merge: false, + merge_strategy: MergeStrategy::Squash, + } + } +@@ -421,41 +421,41 @@ pub struct ArtifactsSettings { + #[serde(deny_unknown_fields)] + pub struct RunLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub goal: Option, ++ pub goal: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub working_dir: Option, ++ pub working_dir: Option, + /// Flat string-to-string map. Replaces wholesale across layers. + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub metadata: HashMap, ++ pub metadata: HashMap, + /// Run inputs: typed scalar values. Replaces wholesale across layers. + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub inputs: Option>, ++ pub inputs: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub model: Option, ++ pub model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub git: Option, ++ pub git: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub prepare: Option, ++ pub prepare: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub execution: Option, ++ pub execution: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub checkpoint: Option, ++ pub checkpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub sandbox: Option, ++ pub sandbox: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub notifications: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub interviews: Option, ++ pub interviews: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub agent: Option, ++ pub agent: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub hooks: Vec, ++ pub hooks: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub scm: Option, ++ pub scm: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub pull_request: Option, ++ pub pull_request: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub artifacts: Option, ++ pub artifacts: Option, + } + + /// The source of a run's goal, either inline literal text or a reference to +@@ -493,7 +493,7 @@ pub enum RunGoalLayer { + /// goals without having to re-walk the layer. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ResolvedRunGoal { +- pub text: String, ++ pub text: String, + pub source: ResolvedGoalSource, + } + +@@ -512,9 +512,9 @@ pub enum ResolvedGoalSource { + #[serde(deny_unknown_fields)] + pub struct RunModelLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provider: Option, ++ pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, ++ pub name: Option, + /// Ordered list of fallback model references. Supports `...` splice marker + /// at layering time — see [`super::splice_array`]. + #[serde(default, skip_serializing_if = "Vec::is_empty")] +@@ -561,7 +561,7 @@ pub struct RunGitLayer { + #[serde(deny_unknown_fields)] + pub struct GitAuthorLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, ++ pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub email: Option, + } +@@ -572,7 +572,7 @@ pub struct GitAuthorLayer { + #[serde(deny_unknown_fields)] + pub struct RunPrepareLayer { + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub steps: Vec, ++ pub steps: Vec, + /// Optional timeout applied to each prepare step. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, +@@ -583,11 +583,11 @@ pub struct RunPrepareLayer { + #[serde(deny_unknown_fields)] + pub struct PrepareStep { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub script: Option, ++ pub script: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub command: Option>, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub env: HashMap, ++ pub env: HashMap, + } + + /// `[run.execution]` — run posture knobs. +@@ -595,12 +595,12 @@ pub struct PrepareStep { + #[serde(deny_unknown_fields)] + pub struct RunExecutionLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub mode: Option, ++ pub mode: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub approval: Option, + /// Positive-form: `true` runs retros, `false` skips them. + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub retros: Option, ++ pub retros: Option, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +@@ -630,18 +630,18 @@ pub struct RunCheckpointLayer { + #[serde(deny_unknown_fields)] + pub struct RunSandboxLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provider: Option, ++ pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub preserve: Option, ++ pub preserve: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub devcontainer: Option, + /// Sticky merge-by-key across layers. + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub env: HashMap, ++ pub env: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub local: Option, ++ pub local: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub daytona: Option, ++ pub daytona: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +@@ -668,26 +668,26 @@ pub struct DaytonaSandboxLayer { + pub auto_stop_interval: Option, + /// Sticky merge-by-key (provider-native labels). + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub labels: HashMap, ++ pub labels: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub snapshot: Option, ++ pub snapshot: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub network: Option, ++ pub network: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub skip_clone: Option, ++ pub skip_clone: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] + #[serde(deny_unknown_fields)] + pub struct DaytonaSnapshotLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, ++ pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub cpu: Option, ++ pub cpu: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub memory: Option, ++ pub memory: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub disk: Option, ++ pub disk: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dockerfile: Option, + } +@@ -712,19 +712,19 @@ pub enum DaytonaNetworkLayer { + #[serde(deny_unknown_fields)] + pub struct NotificationRouteLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub enabled: Option, ++ pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + /// Raw Fabro event names. Splice marker supported at layering time. + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- pub events: Vec, ++ pub events: Vec, + /// Provider-specific destination subtables. First-pass chat providers. + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub slack: Option, ++ pub slack: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub discord: Option, ++ pub discord: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub teams: Option, ++ pub teams: Option, + } + + /// A single string array entry that may be the splice marker. +@@ -769,11 +769,11 @@ pub struct InterviewsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub slack: Option, ++ pub slack: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub discord: Option, ++ pub discord: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub teams: Option, ++ pub teams: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +@@ -791,7 +791,7 @@ pub struct RunAgentLayer { + pub permissions: Option, + /// Agent-scoped MCP server entries, keyed by name. + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub mcps: HashMap, ++ pub mcps: HashMap, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +@@ -810,43 +810,43 @@ pub enum AgentPermissions { + pub enum McpEntryLayer { + Http { + #[serde(default)] +- enabled: Option, +- url: InterpString, ++ enabled: Option, ++ url: InterpString, + #[serde(default)] +- headers: HashMap, ++ headers: HashMap, + #[serde(default)] + startup_timeout: Option, + #[serde(default)] +- tool_timeout: Option, ++ tool_timeout: Option, + }, + Stdio { + #[serde(default)] +- enabled: Option, ++ enabled: Option, + #[serde(default)] +- script: Option, ++ script: Option, + #[serde(default)] +- command: Option>, ++ command: Option>, + #[serde(default)] +- env: HashMap, ++ env: HashMap, + #[serde(default)] + startup_timeout: Option, + #[serde(default)] +- tool_timeout: Option, ++ tool_timeout: Option, + }, + Sandbox { + #[serde(default)] +- enabled: Option, ++ enabled: Option, + #[serde(default)] +- script: Option, ++ script: Option, + #[serde(default)] +- command: Option>, +- port: u16, ++ command: Option>, ++ port: u16, + #[serde(default)] +- env: HashMap, ++ env: HashMap, + #[serde(default)] + startup_timeout: Option, + #[serde(default)] +- tool_timeout: Option, ++ tool_timeout: Option, + }, + } + +@@ -858,40 +858,40 @@ pub enum McpEntryLayer { + pub struct HookEntry { + /// Optional merge identity. Hooks with the same `id` replace in place. + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub id: Option, ++ pub id: Option, + /// Display-only human name. + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, +- pub event: HookEvent, ++ pub name: Option, ++ pub event: HookEvent, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub matcher: Option, ++ pub matcher: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub blocking: Option, ++ pub blocking: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub timeout: Option, ++ pub timeout: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub sandbox: Option, ++ pub sandbox: Option, + // Exactly one of the following groups is expected: + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub script: Option, ++ pub script: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub command: Option>, ++ pub command: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub url: Option, ++ pub url: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub headers: HashMap, ++ pub headers: HashMap, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub allowed_env_vars: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub tls: Option, ++ pub tls: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub prompt: Option, ++ pub prompt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub model: Option, ++ pub model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub max_tool_rounds: Option, ++ pub max_tool_rounds: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub agent: Option, ++ pub agent: Option, + } + + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +@@ -939,14 +939,14 @@ pub enum HookEvent { + #[serde(deny_unknown_fields)] + pub struct RunScmLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provider: Option, ++ pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub owner: Option, ++ pub owner: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// Provider-specific SCM leaves. First-pass providers. + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub github: Option, ++ pub github: Option, + } + + /// `[run.scm.github]` — GitHub-specific SCM leaf. Intentionally minimal in +@@ -961,11 +961,11 @@ pub struct ScmGitHubLayer; + #[serde(deny_unknown_fields)] + pub struct RunPullRequestLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub enabled: Option, ++ pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub draft: Option, ++ pub draft: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub auto_merge: Option, ++ pub auto_merge: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub merge_strategy: Option, + } +diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs +index 42c80ae6..825f0905 100644 +--- a/lib/crates/fabro-types/src/settings/server.rs ++++ b/lib/crates/fabro-types/src/settings/server.rs +@@ -17,15 +17,15 @@ use super::interp::InterpString; + /// A structurally resolved `[server]` view for consumers. + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] + pub struct ServerSettings { +- pub listen: ServerListenSettings, +- pub api: ServerApiSettings, +- pub web: ServerWebSettings, +- pub auth: ServerAuthSettings, +- pub storage: ServerStorageSettings, +- pub artifacts: ServerArtifactsSettings, +- pub slatedb: ServerSlateDbSettings, +- pub scheduler: ServerSchedulerSettings, +- pub logging: ServerLoggingSettings, ++ pub listen: ServerListenSettings, ++ pub api: ServerApiSettings, ++ pub web: ServerWebSettings, ++ pub auth: ServerAuthSettings, ++ pub storage: ServerStorageSettings, ++ pub artifacts: ServerArtifactsSettings, ++ pub slatedb: ServerSlateDbSettings, ++ pub scheduler: ServerSchedulerSettings, ++ pub logging: ServerLoggingSettings, + pub integrations: ServerIntegrationsSettings, + } + +@@ -35,7 +35,7 @@ pub enum ServerListenSettings { + Tcp { + #[serde(serialize_with = "serialize_socket_addr")] + address: SocketAddr, +- tls: Option, ++ tls: Option, + }, + Unix { + path: InterpString, +@@ -53,14 +53,14 @@ impl Default for ServerListenSettings { + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] + pub struct TlsConfig { + pub cert: InterpString, +- pub key: InterpString, ++ pub key: InterpString, + } + + impl Default for TlsConfig { + fn default() -> Self { + Self { + cert: InterpString::parse(""), +- key: InterpString::parse(""), ++ key: InterpString::parse(""), + } + } + } +@@ -73,14 +73,14 @@ pub struct ServerApiSettings { + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] + pub struct ServerWebSettings { + pub enabled: bool, +- pub url: InterpString, ++ pub url: InterpString, + } + + impl Default for ServerWebSettings { + fn default() -> Self { + Self { + enabled: false, +- url: InterpString::parse(""), ++ url: InterpString::parse(""), + } + } + } +@@ -88,14 +88,14 @@ impl Default for ServerWebSettings { + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] + pub struct ServerAuthSettings { + pub methods: Vec, +- pub github: ServerAuthGithubSettings, ++ pub github: ServerAuthGithubSettings, + } + + impl Default for ServerAuthSettings { + fn default() -> Self { + Self { + methods: vec![ServerAuthMethod::DevToken], +- github: ServerAuthGithubSettings::default(), ++ github: ServerAuthGithubSettings::default(), + } + } + } +@@ -128,34 +128,34 @@ impl Default for ServerStorageSettings { + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] + pub struct ServerArtifactsSettings { + pub prefix: InterpString, +- pub store: ObjectStoreSettings, ++ pub store: ObjectStoreSettings, + } + + impl Default for ServerArtifactsSettings { + fn default() -> Self { + Self { + prefix: InterpString::parse(""), +- store: ObjectStoreSettings::default(), ++ store: ObjectStoreSettings::default(), + } + } + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] + pub struct ServerSlateDbSettings { +- pub prefix: InterpString, +- pub store: ObjectStoreSettings, ++ pub prefix: InterpString, ++ pub store: ObjectStoreSettings, + #[serde(serialize_with = "serialize_std_duration")] + pub flush_interval: StdDuration, +- pub disk_cache: bool, ++ pub disk_cache: bool, + } + + impl Default for ServerSlateDbSettings { + fn default() -> Self { + Self { +- prefix: InterpString::parse(""), +- store: ObjectStoreSettings::default(), ++ prefix: InterpString::parse(""), ++ store: ObjectStoreSettings::default(), + flush_interval: StdDuration::ZERO, +- disk_cache: false, ++ disk_cache: false, + } + } + } +@@ -167,9 +167,9 @@ pub enum ObjectStoreSettings { + root: InterpString, + }, + S3 { +- bucket: InterpString, +- region: InterpString, +- endpoint: Option, ++ bucket: InterpString, ++ region: InterpString, ++ endpoint: Option, + path_style: bool, + }, + } +@@ -194,26 +194,26 @@ pub struct ServerLoggingSettings { + + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] + pub struct ServerIntegrationsSettings { +- pub github: GithubIntegrationSettings, +- pub slack: SlackIntegrationSettings, ++ pub github: GithubIntegrationSettings, ++ pub slack: SlackIntegrationSettings, + pub discord: DiscordIntegrationSettings, +- pub teams: TeamsIntegrationSettings, ++ pub teams: TeamsIntegrationSettings, + } + + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] + pub struct GithubIntegrationSettings { +- pub enabled: bool, +- pub strategy: GithubIntegrationStrategy, +- pub app_id: Option, +- pub client_id: Option, +- pub slug: Option, ++ pub enabled: bool, ++ pub strategy: GithubIntegrationStrategy, ++ pub app_id: Option, ++ pub client_id: Option, ++ pub slug: Option, + pub permissions: HashMap, +- pub webhooks: Option, ++ pub webhooks: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] + pub struct SlackIntegrationSettings { +- pub enabled: bool, ++ pub enabled: bool, + pub default_channel: Option, + } + +@@ -251,23 +251,23 @@ where + #[serde(deny_unknown_fields)] + pub struct ServerLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub listen: Option, ++ pub listen: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub api: Option, ++ pub api: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub web: Option, ++ pub web: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub auth: Option, ++ pub auth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub storage: Option, ++ pub storage: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub artifacts: Option, ++ pub artifacts: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub slatedb: Option, ++ pub slatedb: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub scheduler: Option, ++ pub scheduler: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub logging: Option, ++ pub logging: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub integrations: Option, + } +@@ -281,7 +281,7 @@ pub enum ServerListenLayer { + #[serde(default)] + address: Option, + #[serde(default)] +- tls: Option, ++ tls: Option, + }, + Unix { + #[serde(default)] +@@ -295,7 +295,7 @@ pub struct ServerListenTlsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cert: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub key: Option, ++ pub key: Option, + } + + /// `[server.api]` — API surface settings. +@@ -315,7 +315,7 @@ pub struct ServerWebLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub url: Option, ++ pub url: Option, + } + + /// `[server.auth]` — cohesive server auth surface. +@@ -329,7 +329,7 @@ pub struct ServerAuthLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub methods: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub github: Option, ++ pub github: Option, + } + + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +@@ -354,11 +354,11 @@ pub struct ServerArtifactsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub prefix: Option, ++ pub prefix: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub local: Option, ++ pub local: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub s3: Option, ++ pub s3: Option, + } + + /// `[server.slatedb]` — SlateDB bottomless storage plus tunables. +@@ -366,17 +366,17 @@ pub struct ServerArtifactsLayer { + #[serde(deny_unknown_fields)] + pub struct ServerSlateDbLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub provider: Option, ++ pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub prefix: Option, ++ pub prefix: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flush_interval: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub local: Option, ++ pub local: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub s3: Option, ++ pub s3: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub disk_cache: Option, ++ pub disk_cache: Option, + } + + /// Closed enum of object-store providers. Unknown providers hard-fail +@@ -401,11 +401,11 @@ pub struct ObjectStoreLocalLayer { + #[serde(deny_unknown_fields)] + pub struct ObjectStoreS3Layer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub bucket: Option, ++ pub bucket: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub region: Option, ++ pub region: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub endpoint: Option, ++ pub endpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path_style: Option, + } +@@ -434,13 +434,13 @@ pub struct ServerLoggingLayer { + #[serde(deny_unknown_fields)] + pub struct ServerIntegrationsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub github: Option, ++ pub github: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub slack: Option, ++ pub slack: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub discord: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub teams: Option, ++ pub teams: Option, + } + + /// `[server.integrations.github]` — GitHub App, credentials, and inbound +@@ -449,19 +449,19 @@ pub struct ServerIntegrationsLayer { + #[serde(deny_unknown_fields)] + pub struct GithubIntegrationLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub enabled: Option, ++ pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub strategy: Option, ++ pub strategy: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub app_id: Option, ++ pub app_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub client_id: Option, ++ pub client_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub slug: Option, ++ pub slug: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub permissions: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub webhooks: Option, ++ pub webhooks: Option, + } + + /// `[server.integrations.slack]` — Slack workspace credentials and defaults. +@@ -469,7 +469,7 @@ pub struct GithubIntegrationLayer { + #[serde(deny_unknown_fields)] + pub struct SlackIntegrationLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub enabled: Option, ++ pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_channel: Option, + } +diff --git a/lib/crates/fabro-types/src/settings/size.rs b/lib/crates/fabro-types/src/settings/size.rs +index 26803ffe..c4fffb83 100644 +--- a/lib/crates/fabro-types/src/settings/size.rs ++++ b/lib/crates/fabro-types/src/settings/size.rs +@@ -125,7 +125,7 @@ impl FromStr for Size { + other => { + return Err(ParseSizeError::InvalidUnit { + input: input.to_owned(), +- unit: other.to_owned(), ++ unit: other.to_owned(), + }); + } + }; +diff --git a/lib/crates/fabro-types/src/settings/workflow.rs b/lib/crates/fabro-types/src/settings/workflow.rs +index d97c7e74..a98be82d 100644 +--- a/lib/crates/fabro-types/src/settings/workflow.rs ++++ b/lib/crates/fabro-types/src/settings/workflow.rs +@@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; + /// A structurally resolved `[workflow]` view for consumers. + #[derive(Debug, Clone, Default, PartialEq, Serialize)] + pub struct WorkflowSettings { +- pub name: Option, ++ pub name: Option, + pub description: Option, +- pub graph: String, +- pub metadata: HashMap, ++ pub graph: String, ++ pub metadata: HashMap, + } + + /// A sparse `[workflow]` layer as it appears in a single settings file. +@@ -21,12 +21,12 @@ pub struct WorkflowSettings { + #[serde(deny_unknown_fields)] + pub struct WorkflowLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub name: Option, ++ pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Optional override for the default `workflow.fabro` graph path. + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub graph: Option, ++ pub graph: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] +- pub metadata: HashMap, ++ pub metadata: HashMap, + } +diff --git a/lib/crates/fabro-types/src/stage_id.rs b/lib/crates/fabro-types/src/stage_id.rs +index baae5886..eef13577 100644 +--- a/lib/crates/fabro-types/src/stage_id.rs ++++ b/lib/crates/fabro-types/src/stage_id.rs +@@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; + #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub struct StageId { + node_id: String, +- visit: u32, ++ visit: u32, + } + + impl StageId { +@@ -190,11 +190,14 @@ mod tests { + StageId::new("code", 1), + ]; + stages.sort(); +- assert_eq!(stages, vec![ +- StageId::new("build", 1), +- StageId::new("code", 1), +- StageId::new("code", 2), +- ]); ++ assert_eq!( ++ stages, ++ vec![ ++ StageId::new("build", 1), ++ StageId::new("code", 1), ++ StageId::new("code", 2), ++ ] ++ ); + } + + #[test] +diff --git a/lib/crates/fabro-types/src/start.rs b/lib/crates/fabro-types/src/start.rs +index 82c20057..6a78addd 100644 +--- a/lib/crates/fabro-types/src/start.rs ++++ b/lib/crates/fabro-types/src/start.rs +@@ -5,10 +5,10 @@ use crate::run_id::RunId; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct StartRecord { +- pub run_id: RunId, ++ pub run_id: RunId, + pub start_time: DateTime, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub run_branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub base_sha: Option, ++ pub base_sha: Option, + } +diff --git a/lib/crates/fabro-types/src/status.rs b/lib/crates/fabro-types/src/status.rs +index 411496be..7b6f9189 100644 +--- a/lib/crates/fabro-types/src/status.rs ++++ b/lib/crates/fabro-types/src/status.rs +@@ -109,7 +109,7 @@ impl std::error::Error for ParseRunStatusError {} + #[derive(Debug, Clone, PartialEq)] + pub struct InvalidTransition { + pub from: RunStatus, +- pub to: RunStatus, ++ pub to: RunStatus, + } + + impl fmt::Display for InvalidTransition { +@@ -146,9 +146,9 @@ pub enum RunControlAction { + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct RunStatusRecord { +- pub status: RunStatus, ++ pub status: RunStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] +- pub reason: Option, ++ pub reason: Option, + pub updated_at: DateTime, + } + +diff --git a/lib/crates/fabro-types/tests/run_event_serde.rs b/lib/crates/fabro-types/tests/run_event_serde.rs +index 1ccb99cb..8a2e71b6 100644 +--- a/lib/crates/fabro-types/tests/run_event_serde.rs ++++ b/lib/crates/fabro-types/tests/run_event_serde.rs +@@ -37,20 +37,20 @@ fn templated_settings() -> SettingsLayer { + #[test] + fn run_created_props_round_trip_templated_settings() { + let props = RunCreatedProps { +- settings: templated_settings(), +- graph: Graph::new("ship"), +- workflow_source: Some("digraph Ship { start -> exit }".to_string()), +- workflow_config: Some("[run]\ngoal = \"Ship {{ env.TASK }}\"".to_string()), +- labels: BTreeMap::from([("team".to_string(), "platform".to_string())]), +- run_dir: "/tmp/run".to_string(), ++ settings: templated_settings(), ++ graph: Graph::new("ship"), ++ workflow_source: Some("digraph Ship { start -> exit }".to_string()), ++ workflow_config: Some("[run]\ngoal = \"Ship {{ env.TASK }}\"".to_string()), ++ labels: BTreeMap::from([("team".to_string(), "platform".to_string())]), ++ run_dir: "/tmp/run".to_string(), + working_directory: "/tmp/project".to_string(), +- host_repo_path: Some("/tmp/project".to_string()), +- repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()), +- base_branch: Some("main".to_string()), +- workflow_slug: Some("demo".to_string()), +- db_prefix: Some("run_".to_string()), +- provenance: None, +- manifest_blob: None, ++ host_repo_path: Some("/tmp/project".to_string()), ++ repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()), ++ base_branch: Some("main".to_string()), ++ workflow_slug: Some("demo".to_string()), ++ db_prefix: Some("run_".to_string()), ++ provenance: None, ++ manifest_blob: None, + }; + + let json = serde_json::to_value(&props).expect("props should serialize"); +diff --git a/lib/crates/fabro-types/tests/run_record_serde.rs b/lib/crates/fabro-types/tests/run_record_serde.rs +index 9212dc64..bf35b4c2 100644 +--- a/lib/crates/fabro-types/tests/run_record_serde.rs ++++ b/lib/crates/fabro-types/tests/run_record_serde.rs +@@ -39,18 +39,18 @@ fn templated_settings() -> SettingsLayer { + #[test] + fn run_record_round_trips_templated_settings() { + let record = RunRecord { +- run_id: fixtures::RUN_1, +- settings: templated_settings(), +- graph: Graph::new("ship"), +- workflow_slug: Some("demo".to_string()), ++ run_id: fixtures::RUN_1, ++ settings: templated_settings(), ++ graph: Graph::new("ship"), ++ workflow_slug: Some("demo".to_string()), + working_directory: PathBuf::from("/tmp/project"), +- host_repo_path: Some("/tmp/project".to_string()), +- repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()), +- base_branch: Some("main".to_string()), +- labels: HashMap::from([("team".to_string(), "platform".to_string())]), +- provenance: None, +- manifest_blob: None, +- definition_blob: None, ++ host_repo_path: Some("/tmp/project".to_string()), ++ repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()), ++ base_branch: Some("main".to_string()), ++ labels: HashMap::from([("team".to_string(), "platform".to_string())]), ++ provenance: None, ++ manifest_blob: None, ++ definition_blob: None, + }; + + let json = serde_json::to_value(&record).expect("record should serialize"); +diff --git a/lib/crates/fabro-util/build.rs b/lib/crates/fabro-util/build.rs +index 2c007b06..8e7bb647 100644 +--- a/lib/crates/fabro-util/build.rs ++++ b/lib/crates/fabro-util/build.rs +@@ -7,25 +7,25 @@ use serde::Deserialize; + #[derive(Deserialize)] + struct Config { + allowlist: Option, +- rules: Vec, ++ rules: Vec, + } + + #[derive(Deserialize)] + struct GlobalAllowlist { +- regexes: Option>, ++ regexes: Option>, + stopwords: Option>, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Rule { +- id: String, +- regex: String, ++ id: String, ++ regex: String, + #[serde(default)] +- keywords: Vec, +- entropy: Option, ++ keywords: Vec, ++ entropy: Option, + #[serde(default)] +- allowlist: Option, ++ allowlist: Option, + #[allow(dead_code)] + description: Option, + } +@@ -33,8 +33,8 @@ struct Rule { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct RuleAllowlist { +- regexes: Option>, +- stopwords: Option>, ++ regexes: Option>, ++ stopwords: Option>, + regex_target: Option, + } + +diff --git a/lib/crates/fabro-util/src/backoff.rs b/lib/crates/fabro-util/src/backoff.rs +index 35a47a2d..7784dc6e 100644 +--- a/lib/crates/fabro-util/src/backoff.rs ++++ b/lib/crates/fabro-util/src/backoff.rs +@@ -5,18 +5,18 @@ use rand::Rng; + #[derive(Debug, Clone)] + pub struct BackoffPolicy { + pub initial_delay: Duration, +- pub factor: f64, +- pub max_delay: Duration, +- pub jitter: bool, ++ pub factor: f64, ++ pub max_delay: Duration, ++ pub jitter: bool, + } + + impl Default for BackoffPolicy { + fn default() -> Self { + Self { + initial_delay: Duration::from_secs(1), +- factor: 2.0, +- max_delay: Duration::from_secs(60), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(60), ++ jitter: false, + } + } + } +@@ -50,9 +50,9 @@ mod tests { + fn delay_first_attempt() { + let b = BackoffPolicy { + initial_delay: Duration::from_millis(100), +- factor: 2.0, +- max_delay: Duration::from_secs(10), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(10), ++ jitter: false, + }; + assert_eq!(b.delay_for_attempt(1), Duration::from_millis(100)); + } +@@ -61,9 +61,9 @@ mod tests { + fn delay_exponential() { + let b = BackoffPolicy { + initial_delay: Duration::from_millis(100), +- factor: 2.0, +- max_delay: Duration::from_secs(10), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_secs(10), ++ jitter: false, + }; + assert_eq!(b.delay_for_attempt(2), Duration::from_millis(200)); + assert_eq!(b.delay_for_attempt(3), Duration::from_millis(400)); +@@ -74,9 +74,9 @@ mod tests { + fn delay_capped_at_max() { + let b = BackoffPolicy { + initial_delay: Duration::from_millis(100), +- factor: 2.0, +- max_delay: Duration::from_millis(300), +- jitter: false, ++ factor: 2.0, ++ max_delay: Duration::from_millis(300), ++ jitter: false, + }; + assert_eq!(b.delay_for_attempt(1), Duration::from_millis(100)); + assert_eq!(b.delay_for_attempt(2), Duration::from_millis(200)); +@@ -88,9 +88,9 @@ mod tests { + fn delay_with_jitter_within_range() { + let b = BackoffPolicy { + initial_delay: Duration::from_millis(1000), +- factor: 1.0, +- max_delay: Duration::from_secs(10), +- jitter: true, ++ factor: 1.0, ++ max_delay: Duration::from_secs(10), ++ jitter: true, + }; + let base = Duration::from_millis(1000); + let min = base.mul_f64(0.5); +@@ -109,9 +109,9 @@ mod tests { + fn delay_linear_factor() { + let b = BackoffPolicy { + initial_delay: Duration::from_millis(500), +- factor: 1.0, +- max_delay: Duration::from_secs(60), +- jitter: false, ++ factor: 1.0, ++ max_delay: Duration::from_secs(60), ++ jitter: false, + }; + assert_eq!(b.delay_for_attempt(1), Duration::from_millis(500)); + assert_eq!(b.delay_for_attempt(2), Duration::from_millis(500)); +diff --git a/lib/crates/fabro-util/src/check_report.rs b/lib/crates/fabro-util/src/check_report.rs +index c20d307a..bc75905d 100644 +--- a/lib/crates/fabro-util/src/check_report.rs ++++ b/lib/crates/fabro-util/src/check_report.rs +@@ -46,22 +46,22 @@ impl CheckDetail { + + #[derive(Debug, Clone, Serialize)] + pub struct CheckResult { +- pub name: String, +- pub status: CheckStatus, +- pub summary: String, +- pub details: Vec, ++ pub name: String, ++ pub status: CheckStatus, ++ pub summary: String, ++ pub details: Vec, + pub remediation: Option, + } + + #[derive(Debug, Clone, Serialize)] + pub struct CheckSection { +- pub title: String, ++ pub title: String, + pub checks: Vec, + } + + #[derive(Debug, Clone, Serialize)] + pub struct CheckReport { +- pub title: String, ++ pub title: String, + pub sections: Vec, + } + +@@ -213,37 +213,37 @@ mod tests { + + fn pass_check(name: &str) -> CheckResult { + CheckResult { +- name: name.to_string(), +- status: CheckStatus::Pass, +- summary: "all good".to_string(), +- details: vec![CheckDetail::new("everything is fine".to_string())], ++ name: name.to_string(), ++ status: CheckStatus::Pass, ++ summary: "all good".to_string(), ++ details: vec![CheckDetail::new("everything is fine".to_string())], + remediation: None, + } + } + + fn warning_check(name: &str) -> CheckResult { + CheckResult { +- name: name.to_string(), +- status: CheckStatus::Warning, +- summary: "not configured".to_string(), +- details: vec![CheckDetail::new("missing something".to_string())], ++ name: name.to_string(), ++ status: CheckStatus::Warning, ++ summary: "not configured".to_string(), ++ details: vec![CheckDetail::new("missing something".to_string())], + remediation: Some("fix it".to_string()), + } + } + + fn error_check(name: &str) -> CheckResult { + CheckResult { +- name: name.to_string(), +- status: CheckStatus::Error, +- summary: "broken".to_string(), +- details: vec![CheckDetail::new("something is wrong".to_string())], ++ name: name.to_string(), ++ status: CheckStatus::Error, ++ summary: "broken".to_string(), ++ details: vec![CheckDetail::new("something is wrong".to_string())], + remediation: Some("repair it".to_string()), + } + } + + fn report(checks: Vec) -> CheckReport { + CheckReport { +- title: "Test Report".into(), ++ title: "Test Report".into(), + sections: vec![CheckSection { + title: String::new(), + checks, +@@ -428,9 +428,9 @@ mod tests { + #[test] + fn render_uses_custom_title() { + let r = CheckReport { +- title: "My Custom Title".into(), ++ title: "My Custom Title".into(), + sections: vec![CheckSection { +- title: String::new(), ++ title: String::new(), + checks: vec![pass_check("Test")], + }], + }; +@@ -449,10 +449,10 @@ mod tests { + #[test] + fn render_truncates_long_detail_lines() { + let r = report(vec![CheckResult { +- name: "Test".into(), +- status: CheckStatus::Pass, +- summary: "ok".into(), +- details: vec![CheckDetail::new( ++ name: "Test".into(), ++ status: CheckStatus::Pass, ++ summary: "ok".into(), ++ details: vec![CheckDetail::new( + "This is a very long detail line for test".into(), + )], + remediation: None, +@@ -472,10 +472,10 @@ mod tests { + #[test] + fn render_no_truncation_when_fits() { + let r = report(vec![CheckResult { +- name: "Test".into(), +- status: CheckStatus::Pass, +- summary: "ok".into(), +- details: vec![CheckDetail::new("short".into())], ++ name: "Test".into(), ++ status: CheckStatus::Pass, ++ summary: "ok".into(), ++ details: vec![CheckDetail::new("short".into())], + remediation: None, + }]); + let out = r.render(&Styles::new(false), true, None, Some(80)); +@@ -488,10 +488,10 @@ mod tests { + #[test] + fn render_warn_detail_uses_red() { + let r = report(vec![CheckResult { +- name: "Repo".into(), +- status: CheckStatus::Pass, +- summary: "ok".into(), +- details: vec![CheckDetail { ++ name: "Repo".into(), ++ status: CheckStatus::Pass, ++ summary: "ok".into(), ++ details: vec![CheckDetail { + text: "Git clean: false".into(), + warn: true, + }], +@@ -507,10 +507,10 @@ mod tests { + #[test] + fn render_remediation_backticks_no_color() { + let r = report(vec![CheckResult { +- name: "Sandbox".into(), +- status: CheckStatus::Warning, +- summary: "not configured".into(), +- details: Vec::new(), ++ name: "Sandbox".into(), ++ status: CheckStatus::Warning, ++ summary: "not configured".into(), ++ details: Vec::new(), + remediation: Some("Run `fabro secret set KEY` to fix".into()), + }]); + let out = r.render(&Styles::new(false), false, None, None); +@@ -529,10 +529,10 @@ mod tests { + #[test] + fn render_remediation_backticks_with_color() { + let r = report(vec![CheckResult { +- name: "Sandbox".into(), +- status: CheckStatus::Warning, +- summary: "not configured".into(), +- details: Vec::new(), ++ name: "Sandbox".into(), ++ status: CheckStatus::Warning, ++ summary: "not configured".into(), ++ details: Vec::new(), + remediation: Some("Run `fabro secret set KEY` to fix".into()), + }]); + let out = r.render(&Styles::new(true), false, None, None); +diff --git a/lib/crates/fabro-util/src/redact/gitleaks.rs b/lib/crates/fabro-util/src/redact/gitleaks.rs +index 859750ea..e38b3666 100644 +--- a/lib/crates/fabro-util/src/redact/gitleaks.rs ++++ b/lib/crates/fabro-util/src/redact/gitleaks.rs +@@ -215,7 +215,7 @@ impl GitleaksEngine { + + regions.push(Region { + start: secret_match.start(), +- end: secret_match.end(), ++ end: secret_match.end(), + }); + } + } +diff --git a/lib/crates/fabro-util/src/redact/mod.rs b/lib/crates/fabro-util/src/redact/mod.rs +index ba472c91..25127fbd 100644 +--- a/lib/crates/fabro-util/src/redact/mod.rs ++++ b/lib/crates/fabro-util/src/redact/mod.rs +@@ -8,7 +8,7 @@ pub use jsonl::{redact_json_value, redact_jsonl_line}; + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Region { + pub start: usize, +- pub end: usize, ++ pub end: usize, + } + + /// Replace all detected secrets in `s` with "REDACTED". +diff --git a/lib/crates/fabro-util/src/run_log.rs b/lib/crates/fabro-util/src/run_log.rs +index 75ec04a7..05211a64 100644 +--- a/lib/crates/fabro-util/src/run_log.rs ++++ b/lib/crates/fabro-util/src/run_log.rs +@@ -15,14 +15,14 @@ static RUN_LOG: OnceLock = OnceLock::new(); + #[derive(Clone, Debug)] + pub struct RunLogWriter { + active: Arc, +- file: Arc>>>, ++ file: Arc>>>, + } + + impl RunLogWriter { + fn new() -> Self { + Self { + active: Arc::new(AtomicBool::new(false)), +- file: Arc::new(Mutex::new(None)), ++ file: Arc::new(Mutex::new(None)), + } + } + } +@@ -33,7 +33,7 @@ impl<'a> MakeWriter<'a> for RunLogWriter { + fn make_writer(&'a self) -> Self::Writer { + if self.active.load(Ordering::Relaxed) { + RunLogGuard::Active { +- buf: Vec::new(), ++ buf: Vec::new(), + file: self.file.clone(), + } + } else { +@@ -46,7 +46,7 @@ impl<'a> MakeWriter<'a> for RunLogWriter { + pub enum RunLogGuard { + Inactive, + Active { +- buf: Vec, ++ buf: Vec, + file: Arc>>>, + }, + } +diff --git a/lib/crates/fabro-util/src/terminal.rs b/lib/crates/fabro-util/src/terminal.rs +index b65c9bfd..6d8a2d0b 100644 +--- a/lib/crates/fabro-util/src/terminal.rs ++++ b/lib/crates/fabro-util/src/terminal.rs +@@ -4,19 +4,19 @@ use console::Style; + /// Each style is forced on/off based on the `use_color` flag passed to + /// [`Styles::new`]. + pub struct Styles { +- pub use_color: bool, +- pub bold: Style, +- pub dim: Style, +- pub cyan: Style, +- pub green: Style, +- pub yellow: Style, +- pub red: Style, +- pub magenta: Style, +- pub underline: Style, +- pub bold_dim: Style, +- pub bold_cyan: Style, ++ pub use_color: bool, ++ pub bold: Style, ++ pub dim: Style, ++ pub cyan: Style, ++ pub green: Style, ++ pub yellow: Style, ++ pub red: Style, ++ pub magenta: Style, ++ pub underline: Style, ++ pub bold_dim: Style, ++ pub bold_cyan: Style, + pub bold_green: Style, +- pub bold_red: Style, ++ pub bold_red: Style, + } + + impl Styles { +diff --git a/lib/crates/fabro-validate/src/lib.rs b/lib/crates/fabro-validate/src/lib.rs +index 733a3a60..3438d842 100644 +--- a/lib/crates/fabro-validate/src/lib.rs ++++ b/lib/crates/fabro-validate/src/lib.rs +@@ -14,12 +14,12 @@ pub enum Severity { + /// A validation diagnostic produced by a lint rule. + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Diagnostic { +- pub rule: String, ++ pub rule: String, + pub severity: Severity, +- pub message: String, +- pub node_id: Option, +- pub edge: Option<(String, String)>, +- pub fix: Option, ++ pub message: String, ++ pub node_id: Option, ++ pub edge: Option<(String, String)>, ++ pub fix: Option, + } + + /// A lint rule that validates a graph. +@@ -159,12 +159,12 @@ mod tests { + } + fn apply(&self, _graph: &Graph) -> Vec { + vec![Diagnostic { +- rule: "always_warn".to_string(), ++ rule: "always_warn".to_string(), + severity: Severity::Warning, +- message: "custom warning".to_string(), +- node_id: None, +- edge: None, +- fix: None, ++ message: "custom warning".to_string(), ++ node_id: None, ++ edge: None, ++ fix: None, + }] + } + } +diff --git a/lib/crates/fabro-validate/src/rules.rs b/lib/crates/fabro-validate/src/rules.rs +index 5be71432..20ecd7f8 100644 +--- a/lib/crates/fabro-validate/src/rules.rs ++++ b/lib/crates/fabro-validate/src/rules.rs +@@ -57,26 +57,26 @@ impl LintRule for StartNodeRule { + .count(); + if start_count == 0 { + return vec![Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, + message: + "Pipeline must have exactly one start node (shape=Mdiamond or id start/Start)" + .to_string(), +- node_id: None, +- edge: None, +- fix: Some("Add a node with shape=Mdiamond or id 'start'".to_string()), ++ node_id: None, ++ edge: None, ++ fix: Some("Add a node with shape=Mdiamond or id 'start'".to_string()), + }]; + } + if start_count > 1 { + return vec![Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: format!( ++ message: format!( + "Pipeline has {start_count} start nodes but must have exactly one" + ), +- node_id: None, +- edge: None, +- fix: Some("Remove extra start nodes".to_string()), ++ node_id: None, ++ edge: None, ++ fix: Some("Remove extra start nodes".to_string()), + }]; + } + Vec::new() +@@ -106,26 +106,26 @@ impl LintRule for TerminalNodeRule { + .count(); + if terminal_count == 0 { + return vec![Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, + message: + "Pipeline must have exactly one terminal node (shape=Msquare or id exit/end)" + .to_string(), +- node_id: None, +- edge: None, +- fix: Some("Add a node with shape=Msquare or id 'exit'/'end'".to_string()), ++ node_id: None, ++ edge: None, ++ fix: Some("Add a node with shape=Msquare or id 'exit'/'end'".to_string()), + }]; + } + if terminal_count > 1 { + return vec![Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: format!( ++ message: format!( + "Pipeline must have exactly one terminal node, found {terminal_count}" + ), +- node_id: None, +- edge: None, +- fix: Some("Remove extra terminal nodes so exactly one remains".to_string()), ++ node_id: None, ++ edge: None, ++ fix: Some("Remove extra terminal nodes so exactly one remains".to_string()), + }]; + } + Vec::new() +@@ -170,12 +170,12 @@ impl LintRule for ReachabilityRule { + unreachable + .into_iter() + .map(|node_id| Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!("Node '{node_id}' is not reachable from the start node"), +- node_id: Some(node_id.to_string()), +- edge: None, +- fix: Some(format!( ++ message: format!("Node '{node_id}' is not reachable from the start node"), ++ node_id: Some(node_id.to_string()), ++ edge: None, ++ fix: Some(format!( + "Add an edge path from the start node to '{node_id}'" + )), + }) +@@ -197,25 +197,25 @@ impl LintRule for EdgeTargetExistsRule { + for edge in &graph.edges { + if !graph.nodes.contains_key(&edge.to) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: format!( ++ message: format!( + "Edge from '{}' targets non-existent node '{}'", + edge.from, edge.to + ), +- node_id: None, +- edge: Some((edge.from.clone(), edge.to.clone())), +- fix: Some(format!("Define node '{}' or fix the edge target", edge.to)), ++ node_id: None, ++ edge: Some((edge.from.clone(), edge.to.clone())), ++ fix: Some(format!("Define node '{}' or fix the edge target", edge.to)), + }); + } + if !graph.nodes.contains_key(&edge.from) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: format!("Edge source '{}' references non-existent node", edge.from), +- node_id: None, +- edge: Some((edge.from.clone(), edge.to.clone())), +- fix: Some(format!( ++ message: format!("Edge source '{}' references non-existent node", edge.from), ++ node_id: None, ++ edge: Some((edge.from.clone(), edge.to.clone())), ++ fix: Some(format!( + "Define node '{}' or fix the edge source", + edge.from + )), +@@ -242,16 +242,16 @@ impl LintRule for StartNoIncomingRule { + let incoming = graph.incoming_edges(&start.id); + if !incoming.is_empty() { + return vec![Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: format!( ++ message: format!( + "Start node '{}' has {} incoming edge(s) but must have none", + start.id, + incoming.len() + ), +- node_id: Some(start.id.clone()), +- edge: None, +- fix: Some("Remove incoming edges to the start node".to_string()), ++ node_id: Some(start.id.clone()), ++ edge: None, ++ fix: Some("Remove incoming edges to the start node".to_string()), + }]; + } + Vec::new() +@@ -279,16 +279,16 @@ impl LintRule for ExitNoOutgoingRule { + let outgoing = graph.outgoing_edges(&node.id); + if !outgoing.is_empty() { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: format!( ++ message: format!( + "Exit node '{}' has {} outgoing edge(s) but must have none", + node.id, + outgoing.len() + ), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some("Remove outgoing edges from the exit node".to_string()), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some("Remove outgoing edges from the exit node".to_string()), + }); + } + } +@@ -317,15 +317,15 @@ impl LintRule for ConditionSyntaxRule { + } + if let Err(e) = parse_condition(condition) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: format!( ++ message: format!( + "Condition '{condition}' on edge {} -> {} failed parse: {e}", + edge.from, edge.to + ), +- node_id: None, +- edge: Some((edge.from.clone(), edge.to.clone())), +- fix: Some( ++ node_id: None, ++ edge: Some((edge.from.clone(), edge.to.clone())), ++ fix: Some( + "Use key=value, key!=value, key>value, key contains value, \ + key matches pattern, or bare key syntax" + .to_string(), +@@ -354,12 +354,12 @@ impl LintRule for StylesheetSyntaxRule { + match parse_stylesheet(stylesheet) { + Ok(_) => Vec::new(), + Err(e) => vec![Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: format!("Model stylesheet parse error: {e}"), +- node_id: None, +- edge: None, +- fix: Some("Fix the model_stylesheet syntax".to_string()), ++ message: format!("Model stylesheet parse error: {e}"), ++ node_id: None, ++ edge: None, ++ fix: Some("Fix the model_stylesheet syntax".to_string()), + }], + } + } +@@ -397,12 +397,12 @@ impl LintRule for TypeKnownRule { + if let Some(node_type) = node.node_type() { + if !KNOWN_HANDLER_TYPES.contains(&node_type) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!("Node '{}' has unrecognized type '{node_type}'", node.id), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some(format!("Use one of: {}", KNOWN_HANDLER_TYPES.join(", "))), ++ message: format!("Node '{}' has unrecognized type '{node_type}'", node.id), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some(format!("Use one of: {}", KNOWN_HANDLER_TYPES.join(", "))), + }); + } + } +@@ -446,15 +446,15 @@ impl LintRule for FidelityValidRule { + if let Some(fidelity) = node.fidelity() { + if fidelity.parse::().is_err() { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( ++ message: format!( + "Node '{}' has invalid fidelity mode '{fidelity}'", + node.id + ), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some(Self::fix_message()), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some(Self::fix_message()), + }); + } + } +@@ -463,15 +463,15 @@ impl LintRule for FidelityValidRule { + if let Some(fidelity) = edge.fidelity() { + if fidelity.parse::().is_err() { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( ++ message: format!( + "Edge {} -> {} has invalid fidelity mode '{fidelity}'", + edge.from, edge.to + ), +- node_id: None, +- edge: Some((edge.from.clone(), edge.to.clone())), +- fix: Some(Self::fix_message()), ++ node_id: None, ++ edge: Some((edge.from.clone(), edge.to.clone())), ++ fix: Some(Self::fix_message()), + }); + } + } +@@ -479,12 +479,12 @@ impl LintRule for FidelityValidRule { + if let Some(fidelity) = graph.default_fidelity() { + if fidelity.parse::().is_err() { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!("Graph has invalid default_fidelity '{fidelity}'"), +- node_id: None, +- edge: None, +- fix: Some(Self::fix_message()), ++ message: format!("Graph has invalid default_fidelity '{fidelity}'"), ++ node_id: None, ++ edge: None, ++ fix: Some(Self::fix_message()), + }); + } + } +@@ -507,30 +507,30 @@ impl LintRule for RetryTargetExistsRule { + if let Some(target) = node.retry_target() { + if !graph.nodes.contains_key(target) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( ++ message: format!( + "Node '{}' has retry_target '{}' that does not exist", + node.id, target + ), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some(format!("Define node '{target}' or fix retry_target")), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some(format!("Define node '{target}' or fix retry_target")), + }); + } + } + if let Some(target) = node.fallback_retry_target() { + if !graph.nodes.contains_key(target) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( ++ message: format!( + "Node '{}' has fallback_retry_target '{}' that does not exist", + node.id, target + ), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some(format!( ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some(format!( + "Define node '{target}' or fix fallback_retry_target" + )), + }); +@@ -540,26 +540,26 @@ impl LintRule for RetryTargetExistsRule { + if let Some(target) = graph.retry_target() { + if !graph.nodes.contains_key(target) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!("Graph has retry_target '{target}' that does not exist"), +- node_id: None, +- edge: None, +- fix: Some(format!("Define node '{target}' or fix graph retry_target")), ++ message: format!("Graph has retry_target '{target}' that does not exist"), ++ node_id: None, ++ edge: None, ++ fix: Some(format!("Define node '{target}' or fix graph retry_target")), + }); + } + } + if let Some(target) = graph.fallback_retry_target() { + if !graph.nodes.contains_key(target) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( ++ message: format!( + "Graph has fallback_retry_target '{target}' that does not exist" + ), +- node_id: None, +- edge: None, +- fix: Some(format!( ++ node_id: None, ++ edge: None, ++ fix: Some(format!( + "Define node '{target}' or fix graph fallback_retry_target" + )), + }); +@@ -628,15 +628,12 @@ impl LintRule for PromptOnLlmNodesRule { + .is_some_and(|l| !l.is_empty()); + if !has_prompt && !has_label { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( +- "LLM node '{}' has no prompt or label attribute", +- node.id +- ), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some("Add a prompt or label attribute".to_string()), ++ message: format!("LLM node '{}' has no prompt or label attribute", node.id), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some("Add a prompt or label attribute".to_string()), + }); + } + } +@@ -703,12 +700,12 @@ impl LintRule for DirectionValidRule { + return Vec::new(); + } + vec![Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!("Graph has invalid rankdir '{rankdir}'"), +- node_id: None, +- edge: None, +- fix: Some(format!("Use one of: {}", VALID_DIRECTIONS.join(", "))), ++ message: format!("Graph has invalid rankdir '{rankdir}'"), ++ node_id: None, ++ edge: None, ++ fix: Some(format!("Use one of: {}", VALID_DIRECTIONS.join(", "))), + }] + } + } +@@ -732,15 +729,15 @@ impl LintRule for ReservedKeywordNodeIdRule { + .values() + .filter(|node| DOT_RESERVED_KEYWORDS.contains(&node.id.to_lowercase().as_str())) + .map(|node| Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( ++ message: format!( + "Node ID '{}' is a DOT reserved keyword and may cause parsing failures", + node.id + ), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some(format!( ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some(format!( + "Rename '{}' to '{}_step' or another non-reserved ID", + node.id, + node.id.to_lowercase() +@@ -1052,23 +1049,23 @@ impl LintRule for ImportErrorRule { + for node in graph.nodes.values() { + if let Some(AttrValue::String(message)) = node.attrs.get("import_error") { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: message.clone(), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some("Fix the imported workflow or import path".to_string()), ++ message: message.clone(), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some("Fix the imported workflow or import path".to_string()), + }); + } + + if node.attrs.contains_key("import") { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Error, +- message: "unresolved import (no base directory available)".to_string(), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some( ++ message: "unresolved import (no base directory available)".to_string(), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some( + "Load the workflow from a file so imports can resolve relative to it" + .to_string(), + ), +@@ -1148,15 +1145,15 @@ impl LintRule for ThreadIdRequiresFidelityFullRule { + if node.thread_id().is_some() && node.fidelity() != Some("full") && !graph_default_full + { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( ++ message: format!( + "Node '{}' has thread_id but fidelity is not 'full'", + node.id + ), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some(Self::FIX.to_string()), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some(Self::FIX.to_string()), + }); + } + } +@@ -1168,15 +1165,15 @@ impl LintRule for ThreadIdRequiresFidelityFullRule { + graph.nodes.get(&edge.to).and_then(|n| n.fidelity()) == Some("full"); + if !edge_full && !target_full && !graph_default_full { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!( ++ message: format!( + "Edge {} -> {} has thread_id but fidelity is not 'full'", + edge.from, edge.to + ), +- node_id: None, +- edge: Some((edge.from.clone(), edge.to.clone())), +- fix: Some(Self::FIX.to_string()), ++ node_id: None, ++ edge: Some((edge.from.clone(), edge.to.clone())), ++ fix: Some(Self::FIX.to_string()), + }); + } + } +@@ -1184,12 +1181,12 @@ impl LintRule for ThreadIdRequiresFidelityFullRule { + + if graph.default_thread().is_some() && !graph_default_full { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: "Graph has default_thread but default_fidelity is not 'full'".to_string(), +- node_id: None, +- edge: None, +- fix: Some(Self::FIX.to_string()), ++ message: "Graph has default_thread but default_fidelity is not 'full'".to_string(), ++ node_id: None, ++ edge: None, ++ fix: Some(Self::FIX.to_string()), + }); + } + +@@ -1214,12 +1211,12 @@ impl LintRule for SelectionValidRule { + if let Some(sel) = node.attrs.get("selection").and_then(AttrValue::as_str) { + if !VALID_SELECTIONS.contains(&sel) { + diagnostics.push(Diagnostic { +- rule: self.name().to_string(), ++ rule: self.name().to_string(), + severity: Severity::Warning, +- message: format!("Node '{}' has invalid selection mode '{sel}'", node.id), +- node_id: Some(node.id.clone()), +- edge: None, +- fix: Some(format!("Use one of: {}", VALID_SELECTIONS.join(", "))), ++ message: format!("Node '{}' has invalid selection mode '{sel}'", node.id), ++ node_id: Some(node.id.clone()), ++ edge: None, ++ fix: Some(format!("Use one of: {}", VALID_SELECTIONS.join(", "))), + }); + } + } +diff --git a/lib/crates/fabro-vault/src/lib.rs b/lib/crates/fabro-vault/src/lib.rs +index 67624933..d3822d00 100644 +--- a/lib/crates/fabro-vault/src/lib.rs ++++ b/lib/crates/fabro-vault/src/lib.rs +@@ -13,24 +13,24 @@ pub enum SecretType { + + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + pub struct SecretEntry { +- pub value: String, ++ pub value: String, + #[serde(rename = "type", default)] + pub secret_type: SecretType, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, +- pub created_at: String, +- pub updated_at: String, ++ pub created_at: String, ++ pub updated_at: String, + } + + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + pub struct SecretMetadata { +- pub name: String, ++ pub name: String, + #[serde(rename = "type")] + pub secret_type: SecretType, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, +- pub created_at: String, +- pub updated_at: String, ++ pub created_at: String, ++ pub updated_at: String, + } + + #[derive(Debug)] +@@ -68,7 +68,7 @@ impl From for Error { + + #[derive(Debug)] + pub struct Vault { +- path: PathBuf, ++ path: PathBuf, + entries: HashMap, + } + +@@ -136,11 +136,11 @@ impl Vault { + .entries + .iter() + .map(|(name, entry)| SecretMetadata { +- name: name.clone(), ++ name: name.clone(), + secret_type: entry.secret_type, + description: entry.description.clone(), +- created_at: entry.created_at.clone(), +- updated_at: entry.updated_at.clone(), ++ created_at: entry.created_at.clone(), ++ updated_at: entry.updated_at.clone(), + }) + .collect::>(); + data.sort_by(|a, b| a.name.cmp(&b.name)); +@@ -343,10 +343,10 @@ mod tests { + .unwrap(); + + let reloaded = Vault::load(path).unwrap(); +- assert_eq!(reloaded.file_secrets(), vec![( +- "/tmp/key.pem".to_string(), +- "pem".to_string() +- )]); ++ assert_eq!( ++ reloaded.file_secrets(), ++ vec![("/tmp/key.pem".to_string(), "pem".to_string())] ++ ); + } + + #[test] +diff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs +index 3347c717..b6d95323 100644 +--- a/lib/crates/fabro-workflow/src/artifact.rs ++++ b/lib/crates/fabro-workflow/src/artifact.rs +@@ -473,11 +473,11 @@ mod tests { + fn normalize_checkpoint_for_resume_converts_legacy_blob_file_refs_and_drops_preamble() { + let blob_id = fabro_types::RunBlobId::new(b"legacy"); + let mut checkpoint = crate::records::Checkpoint { +- timestamp: chrono::Utc::now(), +- current_node: "work".to_string(), +- completed_nodes: vec!["work".to_string()], +- node_retries: HashMap::new(), +- context_values: HashMap::from([ ++ timestamp: chrono::Utc::now(), ++ current_node: "work".to_string(), ++ completed_nodes: vec!["work".to_string()], ++ node_retries: HashMap::new(), ++ context_values: HashMap::from([ + ( + crate::context::keys::CURRENT_PREAMBLE.to_string(), + serde_json::json!("runtime only"), +@@ -487,7 +487,7 @@ mod tests { + serde_json::json!(format!("file:///sandbox/.fabro/artifacts/{blob_id}.json")), + ), + ]), +- node_outcomes: HashMap::from([( ++ node_outcomes: HashMap::from([( + "work".to_string(), + crate::outcome::Outcome { + context_updates: HashMap::from([( +@@ -499,11 +499,11 @@ mod tests { + ..crate::outcome::Outcome::success() + }, + )]), +- next_node_id: Some("exit".to_string()), +- git_commit_sha: None, +- loop_failure_signatures: HashMap::new(), ++ next_node_id: Some("exit".to_string()), ++ git_commit_sha: None, ++ loop_failure_signatures: HashMap::new(), + restart_failure_signatures: HashMap::new(), +- node_visits: HashMap::new(), ++ node_visits: HashMap::new(), + }; + + normalize_checkpoint_for_resume(&mut checkpoint); +@@ -531,8 +531,8 @@ mod tests { + use std::sync::Mutex; + + struct TestSyncEnv { +- accessible: bool, +- written: Mutex>, ++ accessible: bool, ++ written: Mutex>, + working_dir: String, + } + +diff --git a/lib/crates/fabro-workflow/src/artifact_snapshot.rs b/lib/crates/fabro-workflow/src/artifact_snapshot.rs +index 1a6e69bb..3bc4f641 100644 +--- a/lib/crates/fabro-workflow/src/artifact_snapshot.rs ++++ b/lib/crates/fabro-workflow/src/artifact_snapshot.rs +@@ -9,29 +9,29 @@ use tracing::{debug, warn}; + /// A file discovered by the find command. + #[derive(Debug, Clone)] + pub struct DiscoveredFile { +- pub relative_path: String, +- pub size: u64, ++ pub relative_path: String, ++ pub size: u64, + pub mtime_epoch_secs: f64, + } + + /// Metadata for a single captured artifact file. + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct CapturedArtifactInfo { +- pub path: String, +- pub mime: String, +- pub content_md5: String, ++ pub path: String, ++ pub mime: String, ++ pub content_md5: String, + pub content_sha256: String, +- pub bytes: u64, ++ pub bytes: u64, + } + + /// Summary of an artifact collection run. + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ArtifactCollectionSummary { +- pub files_copied: usize, +- pub total_bytes: u64, +- pub files_skipped: usize, ++ pub files_copied: usize, ++ pub total_bytes: u64, ++ pub files_skipped: usize, + pub download_errors: usize, +- pub hash_errors: usize, ++ pub hash_errors: usize, + pub captured_assets: Vec, + } + +@@ -361,9 +361,9 @@ mod tests { + + /// Minimal mock sandbox for artifact_snapshot tests. + struct AssetMockSandbox { +- files: HashMap, +- exec_result: ExecResult, +- working_dir: &'static str, ++ files: HashMap, ++ exec_result: ExecResult, ++ working_dir: &'static str, + platform_str: &'static str, + } + +@@ -372,10 +372,10 @@ mod tests { + Self { + files, + exec_result: ExecResult { +- stdout: exec_stdout.to_string(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: exec_stdout.to_string(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 10, + }, + working_dir: "/home/test", +@@ -507,8 +507,8 @@ mod tests { + #[test] + fn select_files_skips_old_mtime() { + let discovered = vec![DiscoveredFile { +- relative_path: "test-results/old.xml".to_string(), +- size: 1024, ++ relative_path: "test-results/old.xml".to_string(), ++ size: 1024, + mtime_epoch_secs: 500.0, + }]; + let selected = select_files_to_collect(&discovered, 1000.0); +@@ -518,8 +518,8 @@ mod tests { + #[test] + fn select_files_skips_oversized() { + let discovered = vec![DiscoveredFile { +- relative_path: "test-results/huge.xml".to_string(), +- size: MAX_FILE_SIZE + 1, ++ relative_path: "test-results/huge.xml".to_string(), ++ size: MAX_FILE_SIZE + 1, + mtime_epoch_secs: 2000.0, + }]; + let selected = select_files_to_collect(&discovered, 1000.0); +@@ -530,18 +530,18 @@ mod tests { + fn select_files_sorts_smallest_first() { + let discovered = vec![ + DiscoveredFile { +- relative_path: "a.xml".to_string(), +- size: 3000, ++ relative_path: "a.xml".to_string(), ++ size: 3000, + mtime_epoch_secs: 2000.0, + }, + DiscoveredFile { +- relative_path: "b.xml".to_string(), +- size: 1000, ++ relative_path: "b.xml".to_string(), ++ size: 1000, + mtime_epoch_secs: 2000.0, + }, + DiscoveredFile { +- relative_path: "c.xml".to_string(), +- size: 2000, ++ relative_path: "c.xml".to_string(), ++ size: 2000, + mtime_epoch_secs: 2000.0, + }, + ]; +@@ -556,8 +556,8 @@ mod tests { + fn select_files_enforces_total_budget() { + let discovered: Vec = (0..6) + .map(|i| DiscoveredFile { +- relative_path: format!("file{i}.xml"), +- size: 9 * 1024 * 1024, // 9 MB each ++ relative_path: format!("file{i}.xml"), ++ size: 9 * 1024 * 1024, // 9 MB each + mtime_epoch_secs: 2000.0, + }) + .collect(); +@@ -617,18 +617,18 @@ mod tests { + fn normalize_paths_strips_root_prefix() { + let files = vec![ + DiscoveredFile { +- relative_path: "/workspace/test-results/r.xml".to_string(), +- size: 100, ++ relative_path: "/workspace/test-results/r.xml".to_string(), ++ size: 100, + mtime_epoch_secs: 1000.0, + }, + DiscoveredFile { +- relative_path: "./test-results/s.xml".to_string(), +- size: 200, ++ relative_path: "./test-results/s.xml".to_string(), ++ size: 200, + mtime_epoch_secs: 1000.0, + }, + DiscoveredFile { +- relative_path: "test-results/t.xml".to_string(), +- size: 300, ++ relative_path: "test-results/t.xml".to_string(), ++ size: 300, + mtime_epoch_secs: 1000.0, + }, + ]; +@@ -722,8 +722,8 @@ mod tests { + // Create 150 small, recent files — should be capped at MAX_FILE_COUNT (100) + let discovered: Vec = (0..150) + .map(|i| DiscoveredFile { +- relative_path: format!("file{i}.txt"), +- size: 100, // tiny files, well within total budget ++ relative_path: format!("file{i}.txt"), ++ size: 100, // tiny files, well within total budget + mtime_epoch_secs: 2000.0, + }) + .collect(); +diff --git a/lib/crates/fabro-workflow/src/devcontainer_bridge.rs b/lib/crates/fabro-workflow/src/devcontainer_bridge.rs +index b206e06d..d99c09b2 100644 +--- a/lib/crates/fabro-workflow/src/devcontainer_bridge.rs ++++ b/lib/crates/fabro-workflow/src/devcontainer_bridge.rs +@@ -22,11 +22,11 @@ pub fn snapshot_name_for_dockerfile(dockerfile: &str) -> String { + /// Map a `DevcontainerSpec` to a `DaytonaSnapshotConfig`. + pub fn devcontainer_to_snapshot_config(dc: &DevcontainerSpec) -> DaytonaSnapshotConfig { + DaytonaSnapshotConfig { +- name: snapshot_name_for_dockerfile(&dc.dockerfile), ++ name: snapshot_name_for_dockerfile(&dc.dockerfile), + dockerfile: Some(DockerfileSource::Inline(dc.dockerfile.clone())), +- cpu: None, +- memory: None, +- disk: None, ++ cpu: None, ++ memory: None, ++ disk: None, + } + } + +@@ -46,7 +46,7 @@ pub async fn run_devcontainer_lifecycle( + } + + emitter.emit(&Event::DevcontainerLifecycleStarted { +- phase: phase.to_string(), ++ phase: phase.to_string(), + command_count: commands.len(), + }); + let phase_start = Instant::now(); +@@ -158,7 +158,7 @@ pub async fn run_devcontainer_lifecycle( + + let phase_duration = crate::millis_u64(phase_start.elapsed()); + emitter.emit(&Event::DevcontainerLifecycleCompleted { +- phase: phase.to_string(), ++ phase: phase.to_string(), + duration_ms: phase_duration, + }); + Ok(()) +@@ -228,18 +228,18 @@ mod tests { + + /// Simple test sandbox that records commands and returns a fixed exit code. + struct TestSandbox { +- commands: Mutex>, +- cancel_tokens: Mutex>, +- exit_code: i32, ++ commands: Mutex>, ++ cancel_tokens: Mutex>, ++ exit_code: i32, + wait_for_cancel: bool, + } + + impl TestSandbox { + fn new() -> Self { + Self { +- commands: Mutex::new(Vec::new()), +- cancel_tokens: Mutex::new(Vec::new()), +- exit_code: 0, ++ commands: Mutex::new(Vec::new()), ++ cancel_tokens: Mutex::new(Vec::new()), ++ exit_code: 0, + wait_for_cancel: false, + } + } +@@ -255,9 +255,9 @@ mod tests { + + fn waiting_for_cancel() -> Self { + Self { +- commands: Mutex::new(Vec::new()), +- cancel_tokens: Mutex::new(Vec::new()), +- exit_code: 0, ++ commands: Mutex::new(Vec::new()), ++ cancel_tokens: Mutex::new(Vec::new()), ++ exit_code: 0, + wait_for_cancel: true, + } + } +@@ -314,22 +314,22 @@ mod tests { + let token = cancel_token.ok_or_else(|| "missing cancel token".to_string())?; + token.cancelled().await; + return Ok(ExecResult { +- stdout: String::new(), +- stderr: "cancelled".to_string(), +- exit_code: -1, +- timed_out: true, ++ stdout: String::new(), ++ stderr: "cancelled".to_string(), ++ exit_code: -1, ++ timed_out: true, + duration_ms: 10, + }); + } + Ok(ExecResult { +- stdout: String::new(), +- stderr: if self.exit_code != 0 { ++ stdout: String::new(), ++ stderr: if self.exit_code != 0 { + "command failed".to_string() + } else { + String::new() + }, +- exit_code: self.exit_code, +- timed_out: false, ++ exit_code: self.exit_code, ++ timed_out: false, + duration_ms: 10, + }) + } +@@ -591,21 +591,21 @@ mod tests { + + fn test_devcontainer_config(dockerfile: &str) -> DevcontainerSpec { + DevcontainerSpec { +- dockerfile: dockerfile.to_string(), +- build_context: std::path::PathBuf::from("."), +- build_args: HashMap::new(), +- build_target: None, +- initialize_commands: vec![], +- on_create_commands: vec![], ++ dockerfile: dockerfile.to_string(), ++ build_context: std::path::PathBuf::from("."), ++ build_args: HashMap::new(), ++ build_target: None, ++ initialize_commands: vec![], ++ on_create_commands: vec![], + post_create_commands: vec![], +- post_start_commands: vec![], +- environment: HashMap::new(), +- container_env: HashMap::new(), +- remote_user: None, +- workspace_folder: "/workspaces/test".to_string(), +- forwarded_ports: vec![], +- compose_files: vec![], +- compose_service: None, ++ post_start_commands: vec![], ++ environment: HashMap::new(), ++ container_env: HashMap::new(), ++ remote_user: None, ++ workspace_folder: "/workspaces/test".to_string(), ++ forwarded_ports: vec![], ++ compose_files: vec![], ++ compose_service: None, + } + } + } +diff --git a/lib/crates/fabro-workflow/src/error.rs b/lib/crates/fabro-workflow/src/error.rs +index c4736eb1..a14a63fe 100644 +--- a/lib/crates/fabro-workflow/src/error.rs ++++ b/lib/crates/fabro-workflow/src/error.rs +@@ -202,13 +202,13 @@ pub enum Error { + + #[error("Engine error: {message}")] + Engine { +- message: String, ++ message: String, + failure_class: FailureCategory, + }, + + #[error("Handler error: {message}")] + Handler { +- message: String, ++ message: String, + failure_class: FailureCategory, + }, + +@@ -308,8 +308,8 @@ impl Error { + /// Build a fail `Outcome` with structured `FailureDetail`. + pub fn to_fail_outcome(&self) -> Outcome { + let failure = FailureDetail { +- message: self.to_string(), +- category: self.failure_category(), ++ message: self.to_string(), ++ category: self.failure_category(), + signature: self.failure_signature_hint(), + }; + Outcome { +@@ -392,12 +392,12 @@ mod tests { + fn validation_failed_display() { + let err = Error::ValidationFailed { + diagnostics: vec![Diagnostic { +- rule: "test".to_string(), ++ rule: "test".to_string(), + severity: fabro_validate::Severity::Error, +- message: "missing start node".to_string(), +- node_id: None, +- edge: None, +- fix: None, ++ message: "missing start node".to_string(), ++ node_id: None, ++ edge: None, ++ fix: None, + }], + }; + assert_eq!(err.to_string(), "Validation failed"); +@@ -676,7 +676,7 @@ mod tests { + fn llm_error_display() { + let sdk_err = SdkError::Network { + message: "connection refused".into(), +- source: None, ++ source: None, + }; + let err = Error::Llm(sdk_err); + assert_eq!( +@@ -689,13 +689,13 @@ mod tests { + fn llm_error_retryable_delegates_to_sdk() { + let retryable = Error::Llm(SdkError::Network { + message: "timeout".into(), +- source: None, ++ source: None, + }); + assert!(retryable.is_retryable()); + + let non_retryable = Error::Llm(SdkError::Configuration { + message: "bad config".into(), +- source: None, ++ source: None, + }); + assert!(!non_retryable.is_retryable()); + } +@@ -704,7 +704,7 @@ mod tests { + fn llm_error_from_sdk_error() { + let sdk_err = SdkError::Stream { + message: "broken pipe".into(), +- source: None, ++ source: None, + }; + let err = Error::from(sdk_err); + assert!(matches!(err, Error::Llm(_))); +@@ -755,7 +755,7 @@ mod tests { + #[test] + fn failure_class_llm_rate_limit() { + let err = Error::Llm(SdkError::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), + }); + assert_eq!(err.failure_category(), FailureCategory::TransientInfra); +@@ -764,7 +764,7 @@ mod tests { + #[test] + fn failure_class_llm_context_length() { + let err = Error::Llm(SdkError::Provider { +- kind: ProviderErrorKind::ContextLength, ++ kind: ProviderErrorKind::ContextLength, + detail: Box::new(ProviderErrorDetail::new("too long", "openai")), + }); + assert_eq!(err.failure_category(), FailureCategory::BudgetExhausted); +@@ -773,7 +773,7 @@ mod tests { + #[test] + fn failure_class_llm_auth() { + let err = Error::Llm(SdkError::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), + }); + assert_eq!(err.failure_category(), FailureCategory::Deterministic); +@@ -791,7 +791,7 @@ mod tests { + fn failure_class_llm_timeout() { + let err = Error::Llm(SdkError::RequestTimeout { + message: "timed out".into(), +- source: None, ++ source: None, + }); + assert_eq!(err.failure_category(), FailureCategory::TransientInfra); + } +@@ -801,7 +801,7 @@ mod tests { + #[test] + fn classify_sdk_rate_limit() { + let err = SdkError::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), + }; + assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); +@@ -810,7 +810,7 @@ mod tests { + #[test] + fn classify_sdk_server() { + let err = SdkError::Provider { +- kind: ProviderErrorKind::Server, ++ kind: ProviderErrorKind::Server, + detail: Box::new(ProviderErrorDetail::new("500", "openai")), + }; + assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); +@@ -819,7 +819,7 @@ mod tests { + #[test] + fn classify_sdk_context_length() { + let err = SdkError::Provider { +- kind: ProviderErrorKind::ContextLength, ++ kind: ProviderErrorKind::ContextLength, + detail: Box::new(ProviderErrorDetail::new("too long", "openai")), + }; + assert_eq!(classify_sdk_error(&err), FailureCategory::BudgetExhausted); +@@ -828,7 +828,7 @@ mod tests { + #[test] + fn classify_sdk_quota_exceeded() { + let err = SdkError::Provider { +- kind: ProviderErrorKind::QuotaExceeded, ++ kind: ProviderErrorKind::QuotaExceeded, + detail: Box::new(ProviderErrorDetail::new("out of quota", "openai")), + }; + assert_eq!(classify_sdk_error(&err), FailureCategory::BudgetExhausted); +@@ -837,7 +837,7 @@ mod tests { + #[test] + fn classify_sdk_auth() { + let err = SdkError::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), + }; + assert_eq!(classify_sdk_error(&err), FailureCategory::Deterministic); +@@ -847,7 +847,7 @@ mod tests { + fn classify_sdk_request_timeout() { + let err = SdkError::RequestTimeout { + message: "timed out".into(), +- source: None, ++ source: None, + }; + assert_eq!(classify_sdk_error(&err), FailureCategory::TransientInfra); + } +@@ -1492,7 +1492,7 @@ mod tests { + #[test] + fn failure_signature_hint_llm_returns_some() { + let err = Error::Llm(SdkError::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), + }); + assert_eq!( +@@ -1518,7 +1518,7 @@ mod tests { + #[test] + fn to_fail_outcome_llm_has_class_and_signature() { + let err = Error::Llm(SdkError::Provider { +- kind: ProviderErrorKind::Authentication, ++ kind: ProviderErrorKind::Authentication, + detail: Box::new(ProviderErrorDetail::new("bad key", "openai")), + }); + let outcome = err.to_fail_outcome(); +@@ -1545,7 +1545,7 @@ mod tests { + fn to_fail_outcome_includes_error_message_as_reason() { + let err = Error::Llm(SdkError::Network { + message: "connection refused".into(), +- source: None, ++ source: None, + }); + let outcome = err.to_fail_outcome(); + assert!( +@@ -1560,7 +1560,7 @@ mod tests { + fn to_fail_outcome_no_context_updates() { + let err = Error::Llm(SdkError::Network { + message: "refused".into(), +- source: None, ++ source: None, + }); + let outcome = err.to_fail_outcome(); + assert!(outcome.context_updates.is_empty()); +@@ -1604,19 +1604,19 @@ mod tests { + Error::Validation("bad".into()), + Error::ValidationFailed { + diagnostics: vec![Diagnostic { +- rule: "test".into(), ++ rule: "test".into(), + severity: fabro_validate::Severity::Error, +- message: "bad".into(), +- node_id: None, +- edge: None, +- fix: None, ++ message: "bad".into(), ++ node_id: None, ++ edge: None, ++ fix: None, + }], + }, + Error::engine("engine err"), + Error::handler("handler err"), + Error::Llm(SdkError::Network { + message: "refused".into(), +- source: None, ++ source: None, + }), + Error::Checkpoint("cp err".into()), + Error::Stylesheet("style err".into()), +@@ -1684,7 +1684,7 @@ mod tests { + + // 1. Create SdkError → Error + let sdk_err = SdkError::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), + }; + let arc_err = Error::Llm(sdk_err); +@@ -1700,11 +1700,11 @@ mod tests { + // 3. Outcome → StageFailed event + let failure = outcome.failure.clone().unwrap(); + let event = Event::StageFailed { +- node_id: "code".into(), +- name: "code".into(), +- index: 0, +- failure: failure.clone(), +- will_retry: false, ++ node_id: "code".into(), ++ name: "code".into(), ++ index: 0, ++ failure: failure.clone(), ++ will_retry: false, + duration_ms: 0, + }; + +@@ -1770,7 +1770,7 @@ mod tests { + use fabro_agent::Error as AgentError; + + let err = AgentError::Llm(SdkError::Provider { +- kind: ProviderErrorKind::RateLimit, ++ kind: ProviderErrorKind::RateLimit, + detail: Box::new(ProviderErrorDetail::new("too fast", "openai")), + }); + let json = serde_json::to_string(&err).unwrap(); +diff --git a/lib/crates/fabro-workflow/src/event.rs b/lib/crates/fabro-workflow/src/event.rs +index a11bff16..f339098b 100644 +--- a/lib/crates/fabro-workflow/src/event.rs ++++ b/lib/crates/fabro-workflow/src/event.rs +@@ -33,48 +33,48 @@ use crate::runtime_store::RunStoreHandle; + #[allow(clippy::large_enum_variant)] + pub enum Event { + RunCreated { +- run_id: RunId, +- settings: serde_json::Value, +- graph: serde_json::Value, ++ run_id: RunId, ++ settings: serde_json::Value, ++ graph: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] +- workflow_source: Option, ++ workflow_source: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- workflow_config: Option, +- labels: BTreeMap, +- run_dir: String, ++ workflow_config: Option, ++ labels: BTreeMap, ++ run_dir: String, + working_directory: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- host_repo_path: Option, ++ host_repo_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- repo_origin_url: Option, ++ repo_origin_url: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- base_branch: Option, ++ base_branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- workflow_slug: Option, ++ workflow_slug: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- db_prefix: Option, ++ db_prefix: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- provenance: Option, ++ provenance: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- manifest_blob: Option, ++ manifest_blob: Option, + }, + WorkflowRunStarted { +- name: String, +- run_id: RunId, ++ name: String, ++ run_id: RunId, + #[serde(default, skip_serializing_if = "Option::is_none")] +- base_branch: Option, ++ base_branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- base_sha: Option, ++ base_sha: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- run_branch: Option, ++ run_branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + worktree_dir: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- goal: Option, ++ goal: Option, + }, + RunSubmitted { + #[serde(default, skip_serializing_if = "Option::is_none")] +- reason: Option, ++ reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + definition_blob: Option, + }, +@@ -106,48 +106,48 @@ pub enum Event { + RunUnpaused, + RunRewound { + target_checkpoint_ordinal: usize, +- target_node_id: String, +- target_visit: usize, ++ target_node_id: String, ++ target_visit: usize, + #[serde(default, skip_serializing_if = "Option::is_none")] +- previous_status: Option, ++ previous_status: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- run_commit_sha: Option, ++ run_commit_sha: Option, + }, + WorkflowRunCompleted { +- duration_ms: u64, +- artifact_count: usize, ++ duration_ms: u64, ++ artifact_count: usize, + #[serde(default)] +- status: String, ++ status: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- reason: Option, ++ reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- total_usd_micros: Option, ++ total_usd_micros: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + final_git_commit_sha: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- final_patch: Option, ++ final_patch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- billing: Option, ++ billing: Option, + }, + WorkflowRunFailed { +- error: Error, +- duration_ms: u64, ++ error: Error, ++ duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] +- reason: Option, ++ reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + git_commit_sha: Option, + }, + RunNotice { +- level: RunNoticeLevel, +- code: String, ++ level: RunNoticeLevel, ++ code: String, + message: String, + }, + StageStarted { +- node_id: String, +- name: String, +- index: usize, ++ node_id: String, ++ name: String, ++ index: usize, + handler_type: String, +- attempt: usize, ++ attempt: usize, + max_attempts: usize, + }, + StageCompleted { +@@ -181,61 +181,61 @@ pub enum Event { + max_attempts: usize, + }, + StageFailed { +- node_id: String, +- name: String, +- index: usize, +- failure: FailureDetail, +- will_retry: bool, ++ node_id: String, ++ name: String, ++ index: usize, ++ failure: FailureDetail, ++ will_retry: bool, + duration_ms: u64, + }, + StageRetrying { +- node_id: String, +- name: String, +- index: usize, +- attempt: usize, ++ node_id: String, ++ name: String, ++ index: usize, ++ attempt: usize, + max_attempts: usize, +- delay_ms: u64, ++ delay_ms: u64, + }, + ParallelStarted { +- node_id: String, +- visit: u32, ++ node_id: String, ++ visit: u32, + branch_count: usize, +- join_policy: String, ++ join_policy: String, + }, + ParallelBranchStarted { +- parallel_group_id: StageId, ++ parallel_group_id: StageId, + parallel_branch_id: ParallelBranchId, +- branch: String, +- index: usize, ++ branch: String, ++ index: usize, + }, + ParallelBranchCompleted { +- parallel_group_id: StageId, ++ parallel_group_id: StageId, + parallel_branch_id: ParallelBranchId, +- branch: String, +- index: usize, +- duration_ms: u64, +- status: String, ++ branch: String, ++ index: usize, ++ duration_ms: u64, ++ status: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- head_sha: Option, ++ head_sha: Option, + }, + ParallelCompleted { +- node_id: String, +- visit: u32, +- duration_ms: u64, ++ node_id: String, ++ visit: u32, ++ duration_ms: u64, + success_count: usize, + failure_count: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- results: Vec, ++ results: Vec, + }, + InterviewStarted { +- question_id: String, +- question: String, +- stage: String, +- question_type: String, ++ question_id: String, ++ question: String, ++ stage: String, ++ question_type: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] +- options: Vec, ++ options: Vec, + #[serde(default)] +- allow_freeform: bool, ++ allow_freeform: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + timeout_seconds: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +@@ -243,21 +243,21 @@ pub enum Event { + }, + InterviewCompleted { + question_id: String, +- question: String, +- answer: String, ++ question: String, ++ answer: String, + duration_ms: u64, + }, + InterviewTimeout { + question_id: String, +- question: String, +- stage: String, ++ question: String, ++ stage: String, + duration_ms: u64, + }, + InterviewInterrupted { + question_id: String, +- question: String, +- stage: String, +- reason: String, ++ question: String, ++ stage: String, ++ reason: String, + duration_ms: u64, + }, + CheckpointCompleted { +@@ -287,96 +287,96 @@ pub enum Event { + }, + CheckpointFailed { + node_id: String, +- error: String, ++ error: String, + }, + GitCommit { + #[serde(default, skip_serializing_if = "Option::is_none")] + node_id: Option, +- sha: String, ++ sha: String, + }, + GitPush { +- branch: String, ++ branch: String, + success: bool, + }, + GitBranch { + branch: String, +- sha: String, ++ sha: String, + }, + GitWorktreeAdd { +- path: String, ++ path: String, + branch: String, + }, + GitWorktreeRemove { + path: String, + }, + GitFetch { +- branch: String, ++ branch: String, + success: bool, + }, + GitReset { + sha: String, + }, + EdgeSelected { +- from_node: String, +- to_node: String, +- label: Option, +- condition: Option, ++ from_node: String, ++ to_node: String, ++ label: Option, ++ condition: Option, + /// Which selection step chose this edge (e.g. "condition", + /// "preferred_label", "jump"). +- reason: String, ++ reason: String, + /// The stage's preferred label hint, if any. + #[serde(default, skip_serializing_if = "Option::is_none")] +- preferred_label: Option, ++ preferred_label: Option, + /// The stage's suggested next node IDs, if any. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + suggested_next_ids: Vec, + /// The stage outcome status that influenced routing. +- stage_status: String, ++ stage_status: String, + /// Whether this was a direct jump (bypassing normal edge selection). +- is_jump: bool, ++ is_jump: bool, + }, + LoopRestart { + from_node: String, +- to_node: String, ++ to_node: String, + }, + Prompt { +- stage: String, +- visit: u32, +- text: String, ++ stage: String, ++ visit: u32, ++ text: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- mode: Option, ++ mode: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- model: Option, ++ model: Option, + }, + PromptCompleted { +- node_id: String, ++ node_id: String, + response: String, +- model: String, ++ model: String, + provider: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- billing: Option, ++ billing: Option, + }, + /// Forwarded from an agent session, tagged with the workflow stage. + Agent { +- stage: String, +- visit: u32, +- event: AgentEvent, ++ stage: String, ++ visit: u32, ++ event: AgentEvent, + #[serde(default, skip_serializing_if = "Option::is_none")] +- session_id: Option, ++ session_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + parent_session_id: Option, + }, + SubgraphStarted { +- node_id: String, ++ node_id: String, + start_node: String, + }, + SubgraphCompleted { +- node_id: String, ++ node_id: String, + steps_executed: usize, +- status: String, +- duration_ms: u64, ++ status: String, ++ duration_ms: u64, + }, + /// Forwarded from a sandbox lifecycle operation. + Sandbox { +@@ -384,174 +384,174 @@ pub enum Event { + }, + /// Emitted after the sandbox has been initialized (by engine lifecycle). + SandboxInitialized { +- working_directory: String, +- provider: String, ++ working_directory: String, ++ provider: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- identifier: Option, ++ identifier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + host_working_directory: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- container_mount_point: Option, ++ container_mount_point: Option, + }, + SetupStarted { + command_count: usize, + }, + SetupCommandStarted { + command: String, +- index: usize, ++ index: usize, + }, + SetupCommandCompleted { +- command: String, +- index: usize, +- exit_code: i32, ++ command: String, ++ index: usize, ++ exit_code: i32, + duration_ms: u64, + }, + SetupCompleted { + duration_ms: u64, + }, + SetupFailed { +- command: String, +- index: usize, ++ command: String, ++ index: usize, + exit_code: i32, +- stderr: String, ++ stderr: String, + }, + StallWatchdogTimeout { +- node: String, ++ node: String, + idle_seconds: u64, + }, + ArtifactCaptured { +- node_id: String, +- attempt: u32, +- node_slug: String, +- path: String, +- mime: String, +- content_md5: String, ++ node_id: String, ++ attempt: u32, ++ node_slug: String, ++ path: String, ++ mime: String, ++ content_md5: String, + content_sha256: String, +- bytes: u64, ++ bytes: u64, + }, + SshAccessReady { + ssh_command: String, + }, + Failover { +- stage: String, ++ stage: String, + from_provider: String, +- from_model: String, +- to_provider: String, +- to_model: String, +- error: String, ++ from_model: String, ++ to_provider: String, ++ to_model: String, ++ error: String, + }, + CliEnsureStarted { + cli_name: String, + provider: String, + }, + CliEnsureCompleted { +- cli_name: String, +- provider: String, ++ cli_name: String, ++ provider: String, + already_installed: bool, +- node_installed: bool, +- duration_ms: u64, ++ node_installed: bool, ++ duration_ms: u64, + }, + CliEnsureFailed { +- cli_name: String, +- provider: String, +- error: String, ++ cli_name: String, ++ provider: String, ++ error: String, + duration_ms: u64, + }, + CommandStarted { +- node_id: String, +- script: String, +- command: String, +- language: String, ++ node_id: String, ++ script: String, ++ command: String, ++ language: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + timeout_ms: Option, + }, + CommandCompleted { +- node_id: String, +- stdout: String, +- stderr: String, ++ node_id: String, ++ stdout: String, ++ stderr: String, + #[serde(default, skip_serializing_if = "Option::is_none")] +- exit_code: Option, ++ exit_code: Option, + duration_ms: u64, +- timed_out: bool, ++ timed_out: bool, + }, + AgentCliStarted { +- node_id: String, +- visit: u32, +- mode: String, ++ node_id: String, ++ visit: u32, ++ mode: String, + provider: String, +- model: String, +- command: String, ++ model: String, ++ command: String, + }, + AgentCliCompleted { +- node_id: String, +- stdout: String, +- stderr: String, +- exit_code: i32, ++ node_id: String, ++ stdout: String, ++ stderr: String, ++ exit_code: i32, + duration_ms: u64, + }, + PullRequestCreated { +- pr_url: String, +- pr_number: u64, +- owner: String, +- repo: String, ++ pr_url: String, ++ pr_number: u64, ++ owner: String, ++ repo: String, + base_branch: String, + head_branch: String, +- title: String, +- draft: bool, ++ title: String, ++ draft: bool, + }, + PullRequestFailed { + error: String, + }, + DevcontainerResolved { +- dockerfile_lines: usize, +- environment_count: usize, ++ dockerfile_lines: usize, ++ environment_count: usize, + lifecycle_command_count: usize, +- workspace_folder: String, ++ workspace_folder: String, + }, + DevcontainerLifecycleStarted { +- phase: String, ++ phase: String, + command_count: usize, + }, + DevcontainerLifecycleCommandStarted { +- phase: String, ++ phase: String, + command: String, +- index: usize, ++ index: usize, + }, + DevcontainerLifecycleCommandCompleted { +- phase: String, +- command: String, +- index: usize, +- exit_code: i32, ++ phase: String, ++ command: String, ++ index: usize, ++ exit_code: i32, + duration_ms: u64, + }, + DevcontainerLifecycleCompleted { +- phase: String, ++ phase: String, + duration_ms: u64, + }, + DevcontainerLifecycleFailed { +- phase: String, +- command: String, +- index: usize, ++ phase: String, ++ command: String, ++ index: usize, + exit_code: i32, +- stderr: String, ++ stderr: String, + }, + RetroStarted { + #[serde(default, skip_serializing_if = "Option::is_none")] +- prompt: Option, ++ prompt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- model: Option, ++ model: Option, + }, + RetroCompleted { + duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] +- response: Option, ++ response: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] +- retro: Option, ++ retro: Option, + }, + RetroFailed { +- error: String, ++ error: String, + duration_ms: u64, + }, + } +@@ -1268,15 +1268,15 @@ pub fn event_name(event: &Event) -> &'static str { + + #[derive(Debug, Default)] + struct StoredEventFields { +- session_id: Option, +- parent_session_id: Option, +- node_id: Option, +- node_label: Option, +- stage_id: Option, +- parallel_group_id: Option, ++ session_id: Option, ++ parent_session_id: Option, ++ node_id: Option, ++ node_label: Option, ++ stage_id: Option, ++ parallel_group_id: Option, + parallel_branch_id: Option, +- tool_call_id: Option, +- actor: Option, ++ tool_call_id: Option, ++ actor: Option, + } + + fn default_node_label(node_id: Option<&String>, node_label: Option) -> Option { +@@ -1294,13 +1294,13 @@ fn node_stored_fields(node_id: Option) -> StoredEventFields { + + fn billed_token_counts_from_llm(usage: &LlmTokenCounts) -> BilledTokenCounts { + BilledTokenCounts { +- input_tokens: usage.input_tokens, +- output_tokens: usage.output_tokens, +- total_tokens: usage.total_tokens(), +- reasoning_tokens: usage.reasoning_tokens, +- cache_read_tokens: usage.cache_read_tokens, ++ input_tokens: usage.input_tokens, ++ output_tokens: usage.output_tokens, ++ total_tokens: usage.total_tokens(), ++ reasoning_tokens: usage.reasoning_tokens, ++ cache_read_tokens: usage.cache_read_tokens, + cache_write_tokens: usage.cache_write_tokens, +- total_usd_micros: None, ++ total_usd_micros: None, + } + } + +@@ -1480,21 +1480,20 @@ fn event_body_from_event(event: &Event) -> EventBody { + manifest_blob, + .. + } => EventBody::RunCreated(fabro_types::RunCreatedProps { +- settings: serde_json::from_value(settings.clone()) +- .expect("run.created settings"), +- graph: serde_json::from_value(graph.clone()).expect("run.created graph"), +- workflow_source: workflow_source.clone(), +- workflow_config: workflow_config.clone(), +- labels: labels.clone(), +- run_dir: run_dir.clone(), ++ settings: serde_json::from_value(settings.clone()).expect("run.created settings"), ++ graph: serde_json::from_value(graph.clone()).expect("run.created graph"), ++ workflow_source: workflow_source.clone(), ++ workflow_config: workflow_config.clone(), ++ labels: labels.clone(), ++ run_dir: run_dir.clone(), + working_directory: working_directory.clone(), +- host_repo_path: host_repo_path.clone(), +- repo_origin_url: repo_origin_url.clone(), +- base_branch: base_branch.clone(), +- workflow_slug: workflow_slug.clone(), +- db_prefix: db_prefix.clone(), +- provenance: provenance.clone(), +- manifest_blob: *manifest_blob, ++ host_repo_path: host_repo_path.clone(), ++ repo_origin_url: repo_origin_url.clone(), ++ base_branch: base_branch.clone(), ++ workflow_slug: workflow_slug.clone(), ++ db_prefix: db_prefix.clone(), ++ provenance: provenance.clone(), ++ manifest_blob: *manifest_blob, + }), + Event::WorkflowRunStarted { + name, +@@ -1505,18 +1504,18 @@ fn event_body_from_event(event: &Event) -> EventBody { + goal, + .. + } => EventBody::RunStarted(fabro_types::RunStartedProps { +- name: name.clone(), +- base_branch: base_branch.clone(), +- base_sha: base_sha.clone(), +- run_branch: run_branch.clone(), ++ name: name.clone(), ++ base_branch: base_branch.clone(), ++ base_sha: base_sha.clone(), ++ run_branch: run_branch.clone(), + worktree_dir: worktree_dir.clone(), +- goal: goal.clone(), ++ goal: goal.clone(), + }), + Event::RunSubmitted { + reason, + definition_blob, + } => EventBody::RunSubmitted(fabro_types::RunSubmittedProps { +- reason: *reason, ++ reason: *reason, + definition_blob: *definition_blob, + }), + Event::RunStarting { reason } => { +@@ -1553,10 +1552,10 @@ fn event_body_from_event(event: &Event) -> EventBody { + run_commit_sha, + } => EventBody::RunRewound(fabro_types::RunRewoundProps { + target_checkpoint_ordinal: *target_checkpoint_ordinal, +- target_node_id: target_node_id.clone(), +- target_visit: *target_visit, +- previous_status: previous_status.clone(), +- run_commit_sha: run_commit_sha.clone(), ++ target_node_id: target_node_id.clone(), ++ target_visit: *target_visit, ++ previous_status: previous_status.clone(), ++ run_commit_sha: run_commit_sha.clone(), + }), + Event::WorkflowRunCompleted { + duration_ms, +@@ -1568,14 +1567,14 @@ fn event_body_from_event(event: &Event) -> EventBody { + final_patch, + billing, + } => EventBody::RunCompleted(fabro_types::RunCompletedProps { +- duration_ms: *duration_ms, +- artifact_count: *artifact_count, +- status: status.clone(), +- reason: *reason, +- total_usd_micros: *total_usd_micros, ++ duration_ms: *duration_ms, ++ artifact_count: *artifact_count, ++ status: status.clone(), ++ reason: *reason, ++ total_usd_micros: *total_usd_micros, + final_git_commit_sha: final_git_commit_sha.clone(), +- final_patch: final_patch.clone(), +- billing: billing.clone(), ++ final_patch: final_patch.clone(), ++ billing: billing.clone(), + }), + Event::WorkflowRunFailed { + error, +@@ -1583,9 +1582,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + reason, + git_commit_sha, + } => EventBody::RunFailed(fabro_types::RunFailedProps { +- error: error.to_string(), +- duration_ms: *duration_ms, +- reason: *reason, ++ error: error.to_string(), ++ duration_ms: *duration_ms, ++ reason: *reason, + git_commit_sha: git_commit_sha.clone(), + }), + Event::RunNotice { +@@ -1593,8 +1592,8 @@ fn event_body_from_event(event: &Event) -> EventBody { + code, + message, + } => EventBody::RunNotice(fabro_types::RunNoticeProps { +- level: *level, +- code: code.clone(), ++ level: *level, ++ code: code.clone(), + message: message.clone(), + }), + Event::StageStarted { +@@ -1604,9 +1603,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + max_attempts, + .. + } => EventBody::StageStarted(fabro_types::StageStartedProps { +- index: *index, ++ index: *index, + handler_type: handler_type.clone(), +- attempt: *attempt, ++ attempt: *attempt, + max_attempts: *max_attempts, + }), + Event::StageCompleted { +@@ -1656,9 +1655,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + duration_ms, + .. + } => EventBody::StageFailed(fabro_types::StageFailedProps { +- index: *index, +- failure: Some(failure.clone()), +- will_retry: *will_retry, ++ index: *index, ++ failure: Some(failure.clone()), ++ will_retry: *will_retry, + duration_ms: *duration_ms, + }), + Event::StageRetrying { +@@ -1668,10 +1667,10 @@ fn event_body_from_event(event: &Event) -> EventBody { + delay_ms, + .. + } => EventBody::StageRetrying(fabro_types::StageRetryingProps { +- index: *index, +- attempt: *attempt, ++ index: *index, ++ attempt: *attempt, + max_attempts: *max_attempts, +- delay_ms: *delay_ms, ++ delay_ms: *delay_ms, + }), + Event::ParallelStarted { + visit, +@@ -1679,9 +1678,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + join_policy, + .. + } => EventBody::ParallelStarted(fabro_types::ParallelStartedProps { +- visit: *visit, ++ visit: *visit, + branch_count: *branch_count, +- join_policy: join_policy.clone(), ++ join_policy: join_policy.clone(), + }), + Event::ParallelBranchStarted { index, .. } => { + EventBody::ParallelBranchStarted(fabro_types::ParallelBranchStartedProps { +@@ -1695,10 +1694,10 @@ fn event_body_from_event(event: &Event) -> EventBody { + head_sha, + .. + } => EventBody::ParallelBranchCompleted(fabro_types::ParallelBranchCompletedProps { +- index: *index, ++ index: *index, + duration_ms: *duration_ms, +- status: status.clone(), +- head_sha: head_sha.clone(), ++ status: status.clone(), ++ head_sha: head_sha.clone(), + }), + Event::ParallelCompleted { + visit, +@@ -1708,11 +1707,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + results, + .. + } => EventBody::ParallelCompleted(fabro_types::ParallelCompletedProps { +- visit: *visit, +- duration_ms: *duration_ms, ++ visit: *visit, ++ duration_ms: *duration_ms, + success_count: *success_count, + failure_count: *failure_count, +- results: results.clone(), ++ results: results.clone(), + }), + Event::InterviewStarted { + question_id, +@@ -1724,12 +1723,12 @@ fn event_body_from_event(event: &Event) -> EventBody { + timeout_seconds, + context_display, + } => EventBody::InterviewStarted(fabro_types::InterviewStartedProps { +- question_id: question_id.clone(), +- question: question.clone(), +- stage: stage.clone(), +- question_type: question_type.clone(), +- options: options.clone(), +- allow_freeform: *allow_freeform, ++ question_id: question_id.clone(), ++ question: question.clone(), ++ stage: stage.clone(), ++ question_type: question_type.clone(), ++ options: options.clone(), ++ allow_freeform: *allow_freeform, + timeout_seconds: *timeout_seconds, + context_display: context_display.clone(), + }), +@@ -1740,8 +1739,8 @@ fn event_body_from_event(event: &Event) -> EventBody { + duration_ms, + } => EventBody::InterviewCompleted(fabro_types::InterviewCompletedProps { + question_id: question_id.clone(), +- question: question.clone(), +- answer: answer.clone(), ++ question: question.clone(), ++ answer: answer.clone(), + duration_ms: *duration_ms, + }), + Event::InterviewTimeout { +@@ -1751,8 +1750,8 @@ fn event_body_from_event(event: &Event) -> EventBody { + duration_ms, + } => EventBody::InterviewTimeout(fabro_types::InterviewTimeoutProps { + question_id: question_id.clone(), +- question: question.clone(), +- stage: stage.clone(), ++ question: question.clone(), ++ stage: stage.clone(), + duration_ms: *duration_ms, + }), + Event::InterviewInterrupted { +@@ -1763,9 +1762,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + duration_ms, + } => EventBody::InterviewInterrupted(fabro_types::InterviewInterruptedProps { + question_id: question_id.clone(), +- question: question.clone(), +- stage: stage.clone(), +- reason: reason.clone(), ++ question: question.clone(), ++ stage: stage.clone(), ++ reason: reason.clone(), + duration_ms: *duration_ms, + }), + Event::CheckpointCompleted { +@@ -1805,16 +1804,16 @@ fn event_body_from_event(event: &Event) -> EventBody { + EventBody::GitCommit(fabro_types::GitCommitProps { sha: sha.clone() }) + } + Event::GitPush { branch, success } => EventBody::GitPush(fabro_types::GitPushProps { +- branch: branch.clone(), ++ branch: branch.clone(), + success: *success, + }), + Event::GitBranch { branch, sha } => EventBody::GitBranch(fabro_types::GitBranchProps { + branch: branch.clone(), +- sha: sha.clone(), ++ sha: sha.clone(), + }), + Event::GitWorktreeAdd { path, branch } => { + EventBody::GitWorktreeAdd(fabro_types::GitWorktreeAddProps { +- path: path.clone(), ++ path: path.clone(), + branch: branch.clone(), + }) + } +@@ -1822,7 +1821,7 @@ fn event_body_from_event(event: &Event) -> EventBody { + EventBody::GitWorktreeRemove(fabro_types::GitWorktreeRemoveProps { path: path.clone() }) + } + Event::GitFetch { branch, success } => EventBody::GitFetch(fabro_types::GitFetchProps { +- branch: branch.clone(), ++ branch: branch.clone(), + success: *success, + }), + Event::GitReset { sha } => { +@@ -1839,20 +1838,20 @@ fn event_body_from_event(event: &Event) -> EventBody { + stage_status, + is_jump, + } => EventBody::EdgeSelected(fabro_types::EdgeSelectedProps { +- from_node: from_node.clone(), +- to_node: to_node.clone(), +- label: label.clone(), +- condition: condition.clone(), +- reason: reason.clone(), +- preferred_label: preferred_label.clone(), ++ from_node: from_node.clone(), ++ to_node: to_node.clone(), ++ label: label.clone(), ++ condition: condition.clone(), ++ reason: reason.clone(), ++ preferred_label: preferred_label.clone(), + suggested_next_ids: suggested_next_ids.clone(), +- stage_status: stage_status.clone(), +- is_jump: *is_jump, ++ stage_status: stage_status.clone(), ++ is_jump: *is_jump, + }), + Event::LoopRestart { from_node, to_node } => { + EventBody::LoopRestart(fabro_types::LoopRestartProps { + from_node: from_node.clone(), +- to_node: to_node.clone(), ++ to_node: to_node.clone(), + }) + } + Event::Prompt { +@@ -1863,11 +1862,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + model, + .. + } => EventBody::StagePrompt(fabro_types::StagePromptProps { +- visit: *visit, +- text: text.clone(), +- mode: mode.clone(), ++ visit: *visit, ++ text: text.clone(), ++ mode: mode.clone(), + provider: provider.clone(), +- model: model.clone(), ++ model: model.clone(), + }), + Event::PromptCompleted { + response, +@@ -1877,16 +1876,16 @@ fn event_body_from_event(event: &Event) -> EventBody { + .. + } => EventBody::PromptCompleted(fabro_types::PromptCompletedProps { + response: response.clone(), +- model: model.clone(), ++ model: model.clone(), + provider: provider.clone(), +- billing: billing.clone(), ++ billing: billing.clone(), + }), + Event::Agent { visit, event, .. } => match event { + AgentEvent::SessionStarted { provider, model } => { + EventBody::AgentSessionStarted(fabro_types::AgentSessionStartedProps { + provider: provider.clone(), +- model: model.clone(), +- visit: *visit, ++ model: model.clone(), ++ visit: *visit, + }) + } + AgentEvent::SessionEnded => { +@@ -1898,7 +1897,7 @@ fn event_body_from_event(event: &Event) -> EventBody { + }) + } + AgentEvent::UserInput { text } => EventBody::AgentInput(fabro_types::AgentInputProps { +- text: text.clone(), ++ text: text.clone(), + visit: *visit, + }), + AgentEvent::AssistantMessage { +@@ -1907,21 +1906,21 @@ fn event_body_from_event(event: &Event) -> EventBody { + usage, + tool_call_count, + } => EventBody::AgentMessage(fabro_types::AgentMessageProps { +- text: text.clone(), +- model: model.clone(), +- billing: billed_token_counts_from_llm(usage), ++ text: text.clone(), ++ model: model.clone(), ++ billing: billed_token_counts_from_llm(usage), + tool_call_count: *tool_call_count, +- visit: *visit, ++ visit: *visit, + }), + AgentEvent::ToolCallStarted { + tool_name, + tool_call_id, + arguments, + } => EventBody::AgentToolStarted(fabro_types::AgentToolStartedProps { +- tool_name: tool_name.clone(), ++ tool_name: tool_name.clone(), + tool_call_id: tool_call_id.clone(), +- arguments: arguments.clone(), +- visit: *visit, ++ arguments: arguments.clone(), ++ visit: *visit, + }), + AgentEvent::ToolCallCompleted { + tool_name, +@@ -1929,11 +1928,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + output, + is_error, + } => EventBody::AgentToolCompleted(fabro_types::AgentToolCompletedProps { +- tool_name: tool_name.clone(), ++ tool_name: tool_name.clone(), + tool_call_id: tool_call_id.clone(), +- output: output.clone(), +- is_error: *is_error, +- visit: *visit, ++ output: output.clone(), ++ is_error: *is_error, ++ visit: *visit, + }), + AgentEvent::Error { error } => EventBody::AgentError(fabro_types::AgentErrorProps { + error: serde_json::to_value(error).expect("serializable agent error"), +@@ -1944,10 +1943,10 @@ fn event_body_from_event(event: &Event) -> EventBody { + message, + details, + } => EventBody::AgentWarning(fabro_types::AgentWarningProps { +- kind: kind.clone(), ++ kind: kind.clone(), + message: message.clone(), + details: details.clone(), +- visit: *visit, ++ visit: *visit, + }), + AgentEvent::LoopDetected => { + EventBody::AgentLoopDetected(fabro_types::AgentLoopDetectedProps { visit: *visit }) +@@ -1955,12 +1954,12 @@ fn event_body_from_event(event: &Event) -> EventBody { + AgentEvent::TurnLimitReached { max_turns } => { + EventBody::AgentTurnLimitReached(fabro_types::AgentTurnLimitReachedProps { + max_turns: *max_turns, +- visit: *visit, ++ visit: *visit, + }) + } + AgentEvent::SteeringInjected { text } => { + EventBody::AgentSteeringInjected(fabro_types::AgentSteeringInjectedProps { +- text: text.clone(), ++ text: text.clone(), + visit: *visit, + }) + } +@@ -1968,9 +1967,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + estimated_tokens, + context_window_size, + } => EventBody::AgentCompactionStarted(fabro_types::AgentCompactionStartedProps { +- estimated_tokens: *estimated_tokens, ++ estimated_tokens: *estimated_tokens, + context_window_size: *context_window_size, +- visit: *visit, ++ visit: *visit, + }), + AgentEvent::CompactionCompleted { + original_turn_count, +@@ -1978,11 +1977,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + summary_token_estimate, + tracked_file_count, + } => EventBody::AgentCompactionCompleted(fabro_types::AgentCompactionCompletedProps { +- original_turn_count: *original_turn_count, +- preserved_turn_count: *preserved_turn_count, ++ original_turn_count: *original_turn_count, ++ preserved_turn_count: *preserved_turn_count, + summary_token_estimate: *summary_token_estimate, +- tracked_file_count: *tracked_file_count, +- visit: *visit, ++ tracked_file_count: *tracked_file_count, ++ visit: *visit, + }), + AgentEvent::LlmRetry { + provider, +@@ -1991,12 +1990,12 @@ fn event_body_from_event(event: &Event) -> EventBody { + delay_secs, + error, + } => EventBody::AgentLlmRetry(fabro_types::AgentLlmRetryProps { +- provider: provider.clone(), +- model: model.clone(), +- attempt: *attempt, ++ provider: provider.clone(), ++ model: model.clone(), ++ attempt: *attempt, + delay_secs: *delay_secs, +- error: serde_json::to_value(error).expect("serializable sdk error"), +- visit: *visit, ++ error: serde_json::to_value(error).expect("serializable sdk error"), ++ visit: *visit, + }), + AgentEvent::SubAgentSpawned { + agent_id, +@@ -2004,9 +2003,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + task, + } => EventBody::AgentSubSpawned(fabro_types::AgentSubSpawnedProps { + agent_id: agent_id.clone(), +- depth: *depth, +- task: task.clone(), +- visit: *visit, ++ depth: *depth, ++ task: task.clone(), ++ visit: *visit, + }), + AgentEvent::SubAgentCompleted { + agent_id, +@@ -2014,11 +2013,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + success, + turns_used, + } => EventBody::AgentSubCompleted(fabro_types::AgentSubCompletedProps { +- agent_id: agent_id.clone(), +- depth: *depth, +- success: *success, ++ agent_id: agent_id.clone(), ++ depth: *depth, ++ success: *success, + turns_used: *turns_used, +- visit: *visit, ++ visit: *visit, + }), + AgentEvent::SubAgentFailed { + agent_id, +@@ -2026,15 +2025,15 @@ fn event_body_from_event(event: &Event) -> EventBody { + error, + } => EventBody::AgentSubFailed(fabro_types::AgentSubFailedProps { + agent_id: agent_id.clone(), +- depth: *depth, +- error: serde_json::to_value(error).expect("serializable agent error"), +- visit: *visit, ++ depth: *depth, ++ error: serde_json::to_value(error).expect("serializable agent error"), ++ visit: *visit, + }), + AgentEvent::SubAgentClosed { agent_id, depth } => { + EventBody::AgentSubClosed(fabro_types::AgentSubClosedProps { + agent_id: agent_id.clone(), +- depth: *depth, +- visit: *visit, ++ depth: *depth, ++ visit: *visit, + }) + } + AgentEvent::McpServerReady { +@@ -2042,14 +2041,14 @@ fn event_body_from_event(event: &Event) -> EventBody { + tool_count, + } => EventBody::AgentMcpReady(fabro_types::AgentMcpReadyProps { + server_name: server_name.clone(), +- tool_count: *tool_count, +- visit: *visit, ++ tool_count: *tool_count, ++ visit: *visit, + }), + AgentEvent::McpServerFailed { server_name, error } => { + EventBody::AgentMcpFailed(fabro_types::AgentMcpFailedProps { + server_name: server_name.clone(), +- error: error.clone(), +- visit: *visit, ++ error: error.clone(), ++ visit: *visit, + }) + } + AgentEvent::AssistantTextStart +@@ -2073,8 +2072,8 @@ fn event_body_from_event(event: &Event) -> EventBody { + .. + } => EventBody::SubgraphCompleted(fabro_types::SubgraphCompletedProps { + steps_executed: *steps_executed, +- status: status.clone(), +- duration_ms: *duration_ms, ++ status: status.clone(), ++ duration_ms: *duration_ms, + }), + Event::Sandbox { event } => match event { + SandboxEvent::Initializing { provider } => { +@@ -2090,20 +2089,20 @@ fn event_body_from_event(event: &Event) -> EventBody { + memory, + url, + } => EventBody::SandboxReady(fabro_types::SandboxReadyProps { +- provider: provider.clone(), ++ provider: provider.clone(), + duration_ms: *duration_ms, +- name: name.clone(), +- cpu: *cpu, +- memory: *memory, +- url: url.clone(), ++ name: name.clone(), ++ cpu: *cpu, ++ memory: *memory, ++ url: url.clone(), + }), + SandboxEvent::InitializeFailed { + provider, + error, + duration_ms, + } => EventBody::SandboxFailed(fabro_types::SandboxFailedProps { +- provider: provider.clone(), +- error: error.clone(), ++ provider: provider.clone(), ++ error: error.clone(), + duration_ms: *duration_ms, + }), + SandboxEvent::CleanupStarted { provider } => { +@@ -2115,13 +2114,13 @@ fn event_body_from_event(event: &Event) -> EventBody { + provider, + duration_ms, + } => EventBody::SandboxCleanupCompleted(fabro_types::SandboxCleanupCompletedProps { +- provider: provider.clone(), ++ provider: provider.clone(), + duration_ms: *duration_ms, + }), + SandboxEvent::CleanupFailed { provider, error } => { + EventBody::SandboxCleanupFailed(fabro_types::SandboxCleanupFailedProps { + provider: provider.clone(), +- error: error.clone(), ++ error: error.clone(), + }) + } + SandboxEvent::SnapshotPulling { name } => { +@@ -2129,7 +2128,7 @@ fn event_body_from_event(event: &Event) -> EventBody { + } + SandboxEvent::SnapshotPulled { name, duration_ms } => { + EventBody::SnapshotPulled(fabro_types::SnapshotCompletedProps { +- name: name.clone(), ++ name: name.clone(), + duration_ms: *duration_ms, + }) + } +@@ -2141,31 +2140,31 @@ fn event_body_from_event(event: &Event) -> EventBody { + } + SandboxEvent::SnapshotReady { name, duration_ms } => { + EventBody::SnapshotReady(fabro_types::SnapshotCompletedProps { +- name: name.clone(), ++ name: name.clone(), + duration_ms: *duration_ms, + }) + } + SandboxEvent::SnapshotFailed { name, error } => { + EventBody::SnapshotFailed(fabro_types::SnapshotFailedProps { +- name: name.clone(), ++ name: name.clone(), + error: error.clone(), + }) + } + SandboxEvent::GitCloneStarted { url, branch } => { + EventBody::GitCloneStarted(fabro_types::GitCloneStartedProps { +- url: url.clone(), ++ url: url.clone(), + branch: branch.clone(), + }) + } + SandboxEvent::GitCloneCompleted { url, duration_ms } => { + EventBody::GitCloneCompleted(fabro_types::GitCloneCompletedProps { +- url: url.clone(), ++ url: url.clone(), + duration_ms: *duration_ms, + }) + } + SandboxEvent::GitCloneFailed { url, error } => { + EventBody::GitCloneFailed(fabro_types::GitCloneFailedProps { +- url: url.clone(), ++ url: url.clone(), + error: error.clone(), + }) + } +@@ -2177,11 +2176,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + host_working_directory, + container_mount_point, + } => EventBody::SandboxInitialized(fabro_types::SandboxInitializedProps { +- working_directory: working_directory.clone(), +- provider: provider.clone(), +- identifier: identifier.clone(), ++ working_directory: working_directory.clone(), ++ provider: provider.clone(), ++ identifier: identifier.clone(), + host_working_directory: host_working_directory.clone(), +- container_mount_point: container_mount_point.clone(), ++ container_mount_point: container_mount_point.clone(), + }), + Event::SetupStarted { command_count } => { + EventBody::SetupStarted(fabro_types::SetupStartedProps { +@@ -2191,7 +2190,7 @@ fn event_body_from_event(event: &Event) -> EventBody { + Event::SetupCommandStarted { command, index } => { + EventBody::SetupCommandStarted(fabro_types::SetupCommandStartedProps { + command: command.clone(), +- index: *index, ++ index: *index, + }) + } + Event::SetupCommandCompleted { +@@ -2200,9 +2199,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + exit_code, + duration_ms, + } => EventBody::SetupCommandCompleted(fabro_types::SetupCommandCompletedProps { +- command: command.clone(), +- index: *index, +- exit_code: *exit_code, ++ command: command.clone(), ++ index: *index, ++ exit_code: *exit_code, + duration_ms: *duration_ms, + }), + Event::SetupCompleted { duration_ms } => { +@@ -2216,10 +2215,10 @@ fn event_body_from_event(event: &Event) -> EventBody { + exit_code, + stderr, + } => EventBody::SetupFailed(fabro_types::SetupFailedProps { +- command: command.clone(), +- index: *index, ++ command: command.clone(), ++ index: *index, + exit_code: *exit_code, +- stderr: stderr.clone(), ++ stderr: stderr.clone(), + }), + Event::StallWatchdogTimeout { idle_seconds, .. } => { + EventBody::StallWatchdogTimeout(fabro_types::StallWatchdogTimeoutProps { +@@ -2236,13 +2235,13 @@ fn event_body_from_event(event: &Event) -> EventBody { + bytes, + .. + } => EventBody::ArtifactCaptured(fabro_types::ArtifactCapturedProps { +- attempt: *attempt, +- node_slug: node_slug.clone(), +- path: path.clone(), +- mime: mime.clone(), +- content_md5: content_md5.clone(), ++ attempt: *attempt, ++ node_slug: node_slug.clone(), ++ path: path.clone(), ++ mime: mime.clone(), ++ content_md5: content_md5.clone(), + content_sha256: content_sha256.clone(), +- bytes: *bytes, ++ bytes: *bytes, + }), + Event::SshAccessReady { ssh_command } => { + EventBody::SshAccessReady(fabro_types::SshAccessReadyProps { +@@ -2258,10 +2257,10 @@ fn event_body_from_event(event: &Event) -> EventBody { + .. + } => EventBody::Failover(fabro_types::FailoverProps { + from_provider: from_provider.clone(), +- from_model: from_model.clone(), +- to_provider: to_provider.clone(), +- to_model: to_model.clone(), +- error: error.clone(), ++ from_model: from_model.clone(), ++ to_provider: to_provider.clone(), ++ to_model: to_model.clone(), ++ error: error.clone(), + }), + Event::CliEnsureStarted { cli_name, provider } => { + EventBody::CliEnsureStarted(fabro_types::CliEnsureStartedProps { +@@ -2276,11 +2275,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + node_installed, + duration_ms, + } => EventBody::CliEnsureCompleted(fabro_types::CliEnsureCompletedProps { +- cli_name: cli_name.clone(), +- provider: provider.clone(), ++ cli_name: cli_name.clone(), ++ provider: provider.clone(), + already_installed: *already_installed, +- node_installed: *node_installed, +- duration_ms: *duration_ms, ++ node_installed: *node_installed, ++ duration_ms: *duration_ms, + }), + Event::CliEnsureFailed { + cli_name, +@@ -2288,9 +2287,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + error, + duration_ms, + } => EventBody::CliEnsureFailed(fabro_types::CliEnsureFailedProps { +- cli_name: cli_name.clone(), +- provider: provider.clone(), +- error: error.clone(), ++ cli_name: cli_name.clone(), ++ provider: provider.clone(), ++ error: error.clone(), + duration_ms: *duration_ms, + }), + Event::CommandStarted { +@@ -2300,9 +2299,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + timeout_ms, + .. + } => EventBody::CommandStarted(fabro_types::CommandStartedProps { +- script: script.clone(), +- command: command.clone(), +- language: language.clone(), ++ script: script.clone(), ++ command: command.clone(), ++ language: language.clone(), + timeout_ms: *timeout_ms, + }), + Event::CommandCompleted { +@@ -2313,11 +2312,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + timed_out, + .. + } => EventBody::CommandCompleted(fabro_types::CommandCompletedProps { +- stdout: stdout.clone(), +- stderr: stderr.clone(), +- exit_code: *exit_code, ++ stdout: stdout.clone(), ++ stderr: stderr.clone(), ++ exit_code: *exit_code, + duration_ms: *duration_ms, +- timed_out: *timed_out, ++ timed_out: *timed_out, + }), + Event::AgentCliStarted { + visit, +@@ -2327,11 +2326,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + command, + .. + } => EventBody::AgentCliStarted(fabro_types::AgentCliStartedProps { +- visit: *visit, +- mode: mode.clone(), ++ visit: *visit, ++ mode: mode.clone(), + provider: provider.clone(), +- model: model.clone(), +- command: command.clone(), ++ model: model.clone(), ++ command: command.clone(), + }), + Event::AgentCliCompleted { + stdout, +@@ -2340,9 +2339,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + duration_ms, + .. + } => EventBody::AgentCliCompleted(fabro_types::AgentCliCompletedProps { +- stdout: stdout.clone(), +- stderr: stderr.clone(), +- exit_code: *exit_code, ++ stdout: stdout.clone(), ++ stderr: stderr.clone(), ++ exit_code: *exit_code, + duration_ms: *duration_ms, + }), + Event::PullRequestCreated { +@@ -2355,14 +2354,14 @@ fn event_body_from_event(event: &Event) -> EventBody { + title, + draft, + } => EventBody::PullRequestCreated(fabro_types::PullRequestCreatedProps { +- pr_url: pr_url.clone(), +- pr_number: *pr_number, +- owner: owner.clone(), +- repo: repo.clone(), ++ pr_url: pr_url.clone(), ++ pr_number: *pr_number, ++ owner: owner.clone(), ++ repo: repo.clone(), + base_branch: base_branch.clone(), + head_branch: head_branch.clone(), +- title: title.clone(), +- draft: *draft, ++ title: title.clone(), ++ draft: *draft, + }), + Event::PullRequestFailed { error } => { + EventBody::PullRequestFailed(fabro_types::PullRequestFailedProps { +@@ -2375,17 +2374,17 @@ fn event_body_from_event(event: &Event) -> EventBody { + lifecycle_command_count, + workspace_folder, + } => EventBody::DevcontainerResolved(fabro_types::DevcontainerResolvedProps { +- dockerfile_lines: *dockerfile_lines, +- environment_count: *environment_count, ++ dockerfile_lines: *dockerfile_lines, ++ environment_count: *environment_count, + lifecycle_command_count: *lifecycle_command_count, +- workspace_folder: workspace_folder.clone(), ++ workspace_folder: workspace_folder.clone(), + }), + Event::DevcontainerLifecycleStarted { + phase, + command_count, + } => EventBody::DevcontainerLifecycleStarted( + fabro_types::DevcontainerLifecycleStartedProps { +- phase: phase.clone(), ++ phase: phase.clone(), + command_count: *command_count, + }, + ), +@@ -2395,9 +2394,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + index, + } => EventBody::DevcontainerLifecycleCommandStarted( + fabro_types::DevcontainerLifecycleCommandStartedProps { +- phase: phase.clone(), ++ phase: phase.clone(), + command: command.clone(), +- index: *index, ++ index: *index, + }, + ), + Event::DevcontainerLifecycleCommandCompleted { +@@ -2408,17 +2407,17 @@ fn event_body_from_event(event: &Event) -> EventBody { + duration_ms, + } => EventBody::DevcontainerLifecycleCommandCompleted( + fabro_types::DevcontainerLifecycleCommandCompletedProps { +- phase: phase.clone(), +- command: command.clone(), +- index: *index, +- exit_code: *exit_code, ++ phase: phase.clone(), ++ command: command.clone(), ++ index: *index, ++ exit_code: *exit_code, + duration_ms: *duration_ms, + }, + ), + Event::DevcontainerLifecycleCompleted { phase, duration_ms } => { + EventBody::DevcontainerLifecycleCompleted( + fabro_types::DevcontainerLifecycleCompletedProps { +- phase: phase.clone(), ++ phase: phase.clone(), + duration_ms: *duration_ms, + }, + ) +@@ -2431,11 +2430,11 @@ fn event_body_from_event(event: &Event) -> EventBody { + stderr, + } => { + EventBody::DevcontainerLifecycleFailed(fabro_types::DevcontainerLifecycleFailedProps { +- phase: phase.clone(), +- command: command.clone(), +- index: *index, ++ phase: phase.clone(), ++ command: command.clone(), ++ index: *index, + exit_code: *exit_code, +- stderr: stderr.clone(), ++ stderr: stderr.clone(), + }) + } + Event::RetroStarted { +@@ -2443,9 +2442,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + provider, + model, + } => EventBody::RetroStarted(fabro_types::RetroStartedProps { +- prompt: prompt.clone(), ++ prompt: prompt.clone(), + provider: provider.clone(), +- model: model.clone(), ++ model: model.clone(), + }), + Event::RetroCompleted { + duration_ms, +@@ -2453,12 +2452,12 @@ fn event_body_from_event(event: &Event) -> EventBody { + retro, + } => EventBody::RetroCompleted(fabro_types::RetroCompletedProps { + duration_ms: *duration_ms, +- response: response.clone(), +- retro: retro.clone(), ++ response: response.clone(), ++ retro: retro.clone(), + }), + Event::RetroFailed { error, duration_ms } => { + EventBody::RetroFailed(fabro_types::RetroFailedProps { +- error: error.clone(), ++ error: error.clone(), + duration_ms: *duration_ms, + }) + } +@@ -2470,9 +2469,9 @@ fn event_body_from_event(event: &Event) -> EventBody { + /// that happen inside a concrete stage execution. + #[derive(Clone, Debug)] + pub struct StageScope { +- pub node_id: String, +- pub visit: u32, +- pub parallel_group_id: Option, ++ pub node_id: String, ++ pub visit: u32, ++ pub parallel_group_id: Option, + pub parallel_branch_id: Option, + } + +@@ -2481,9 +2480,9 @@ impl StageScope { + /// ids from the current context. + pub fn from_context(context: &WfContext, node_id: impl Into) -> Self { + Self { +- node_id: node_id.into(), +- visit: u32::try_from(visit_from_context(context)).unwrap_or(u32::MAX), +- parallel_group_id: context.parallel_group_id(), ++ node_id: node_id.into(), ++ visit: u32::try_from(visit_from_context(context)).unwrap_or(u32::MAX), ++ parallel_group_id: context.parallel_group_id(), + parallel_branch_id: context.parallel_branch_id(), + } + } +@@ -2515,9 +2514,9 @@ impl StageScope { + parallel_branch_id: ParallelBranchId, + ) -> Self { + Self { +- node_id: target_node_id.into(), +- visit: target_visit, +- parallel_group_id: Some(parallel_group_id), ++ node_id: target_node_id.into(), ++ visit: target_visit, ++ parallel_group_id: Some(parallel_group_id), + parallel_branch_id: Some(parallel_branch_id), + } + } +@@ -2762,8 +2761,8 @@ type EventListener = Arc; + + /// Callback-based event emitter for workflow run events. + pub struct Emitter { +- run_id: RunId, +- listeners: std::sync::Mutex>, ++ run_id: RunId, ++ listeners: std::sync::Mutex>, + /// Epoch milliseconds of the last `emit()` or `touch()` call. 0 until first + /// event. + last_event_at: AtomicI64, +@@ -2896,13 +2895,13 @@ mod tests { + received_clone.lock().unwrap().push(event.clone()); + }); + emitter.emit(&Event::WorkflowRunStarted { +- name: "test".to_string(), +- run_id: fixtures::RUN_1, +- base_branch: None, +- base_sha: None, +- run_branch: None, ++ name: "test".to_string(), ++ run_id: fixtures::RUN_1, ++ base_branch: None, ++ base_sha: None, ++ run_branch: None, + worktree_dir: None, +- goal: None, ++ goal: None, + }); + let events = received.lock().unwrap(); + assert_eq!(events.len(), 1); +@@ -2945,9 +2944,9 @@ mod tests { + }, + Utc::now(), + Some(&StageScope { +- node_id: "plan".to_string(), +- visit: 1, +- parallel_group_id: None, ++ node_id: "plan".to_string(), ++ visit: 1, ++ parallel_group_id: None, + parallel_branch_id: None, + }), + ); +@@ -2965,28 +2964,31 @@ mod tests { + + #[test] + fn run_event_stage_completed_keeps_response_and_signature_snapshots() { +- let stored = to_run_event(&fixtures::RUN_2, &Event::StageCompleted { +- node_id: "plan".to_string(), +- name: "Plan".to_string(), +- index: 0, +- duration_ms: 5000, +- status: "success".to_string(), +- preferred_label: None, +- suggested_next_ids: Vec::new(), +- billing: None, +- failure: None, +- notes: None, +- files_touched: Vec::new(), +- context_updates: None, +- jump_to_node: None, +- context_values: None, +- node_visits: None, +- loop_failure_signatures: Some(BTreeMap::from([("sig-a".to_string(), 2usize)])), +- restart_failure_signatures: Some(BTreeMap::from([("sig-b".to_string(), 1usize)])), +- response: Some("done".to_string()), +- attempt: 1, +- max_attempts: 1, +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_2, ++ &Event::StageCompleted { ++ node_id: "plan".to_string(), ++ name: "Plan".to_string(), ++ index: 0, ++ duration_ms: 5000, ++ status: "success".to_string(), ++ preferred_label: None, ++ suggested_next_ids: Vec::new(), ++ billing: None, ++ failure: None, ++ notes: None, ++ files_touched: Vec::new(), ++ context_updates: None, ++ jump_to_node: None, ++ context_values: None, ++ node_visits: None, ++ loop_failure_signatures: Some(BTreeMap::from([("sig-a".to_string(), 2usize)])), ++ restart_failure_signatures: Some(BTreeMap::from([("sig-b".to_string(), 1usize)])), ++ response: Some("done".to_string()), ++ attempt: 1, ++ max_attempts: 1, ++ }, ++ ); + + let properties = stored.properties().unwrap(); + assert_eq!(properties["response"], "done"); +@@ -2996,17 +2998,20 @@ mod tests { + + #[test] + fn run_event_stage_failure_keeps_failure_detail() { +- let stored = to_run_event(&fixtures::RUN_3, &Event::StageFailed { +- node_id: "code".to_string(), +- name: "Code".to_string(), +- index: 1, +- failure: FailureDetail::new( +- "lint failed", +- crate::outcome::FailureCategory::Deterministic, +- ), +- will_retry: true, +- duration_ms: 5000, +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_3, ++ &Event::StageFailed { ++ node_id: "code".to_string(), ++ name: "Code".to_string(), ++ index: 1, ++ failure: FailureDetail::new( ++ "lint failed", ++ crate::outcome::FailureCategory::Deterministic, ++ ), ++ will_retry: true, ++ duration_ms: 5000, ++ }, ++ ); + + assert_eq!(stored.event_name(), "stage.failed"); + let properties = stored.properties().unwrap(); +@@ -3017,17 +3022,20 @@ mod tests { + + #[test] + fn run_event_agent_tool_started_moves_session_metadata_to_header() { +- let stored = to_run_event(&fixtures::RUN_4, &Event::Agent { +- stage: "code".to_string(), +- visit: 2, +- event: AgentEvent::ToolCallStarted { +- tool_name: "read_file".to_string(), +- tool_call_id: "call_1".to_string(), +- arguments: serde_json::json!({"path": "src/main.rs"}), ++ let stored = to_run_event( ++ &fixtures::RUN_4, ++ &Event::Agent { ++ stage: "code".to_string(), ++ visit: 2, ++ event: AgentEvent::ToolCallStarted { ++ tool_name: "read_file".to_string(), ++ tool_call_id: "call_1".to_string(), ++ arguments: serde_json::json!({"path": "src/main.rs"}), ++ }, ++ session_id: Some("ses_child".to_string()), ++ parent_session_id: Some("ses_parent".to_string()), + }, +- session_id: Some("ses_child".to_string()), +- parent_session_id: Some("ses_parent".to_string()), +- }); ++ ); + + assert_eq!(stored.event_name(), "agent.tool.started"); + assert_eq!(stored.node_id.as_deref(), Some("code")); +@@ -3042,16 +3050,19 @@ mod tests { + + #[test] + fn run_event_sandbox_event_keeps_properties_nested() { +- let stored = to_run_event(&fixtures::RUN_5, &Event::Sandbox { +- event: SandboxEvent::Ready { +- provider: "daytona".to_string(), +- duration_ms: 2500, +- name: Some("sandbox-1".to_string()), +- cpu: Some(4.0), +- memory: Some(8.0), +- url: Some("https://example.test".to_string()), ++ let stored = to_run_event( ++ &fixtures::RUN_5, ++ &Event::Sandbox { ++ event: SandboxEvent::Ready { ++ provider: "daytona".to_string(), ++ duration_ms: 2500, ++ name: Some("sandbox-1".to_string()), ++ cpu: Some(4.0), ++ memory: Some(8.0), ++ url: Some("https://example.test".to_string()), ++ }, + }, +- }); ++ ); + + assert_eq!(stored.event_name(), "sandbox.ready"); + assert!(stored.node_id.is_none()); +@@ -3062,12 +3073,15 @@ mod tests { + + #[test] + fn run_event_workflow_failure_uses_display_error() { +- let stored = to_run_event(&fixtures::RUN_6, &Event::WorkflowRunFailed { +- error: Error::handler("boom"), +- duration_ms: 900, +- reason: Some(StatusReason::WorkflowError), +- git_commit_sha: Some("abc123".to_string()), +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_6, ++ &Event::WorkflowRunFailed { ++ error: Error::handler("boom"), ++ duration_ms: 900, ++ reason: Some(StatusReason::WorkflowError), ++ git_commit_sha: Some("abc123".to_string()), ++ }, ++ ); + + assert_eq!(stored.event_name(), "run.failed"); + let properties = stored.properties().unwrap(); +@@ -3084,11 +3098,14 @@ mod tests { + None, + ); + let run_store = store.create_run(&fixtures::RUN_7).await.unwrap(); +- let stored = to_run_event(&fixtures::RUN_7, &Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "example".to_string(), +- message: "notice".to_string(), +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_7, ++ &Event::RunNotice { ++ level: RunNoticeLevel::Warn, ++ code: "example".to_string(), ++ message: "notice".to_string(), ++ }, ++ ); + let payload = build_redacted_event_payload(&stored, &fixtures::RUN_7).unwrap(); + run_store.append_event(&payload).await.unwrap(); + +@@ -3145,11 +3162,14 @@ mod tests { + + #[test] + fn build_redacted_event_payload_requires_id() { +- let stored = to_run_event(&fixtures::RUN_8, &Event::RetroStarted { +- prompt: Some("Analyze the run".to_string()), +- provider: None, +- model: None, +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_8, ++ &Event::RetroStarted { ++ prompt: Some("Analyze the run".to_string()), ++ provider: None, ++ model: None, ++ }, ++ ); + let payload = build_redacted_event_payload(&stored, &fixtures::RUN_8).unwrap(); + assert_eq!(payload.as_value()["id"], stored.id); + assert_eq!(payload.as_value()["event"], "retro.started"); +@@ -3163,31 +3183,31 @@ mod tests { + fn event_name_matches_new_dot_notation() { + assert_eq!( + event_name(&Event::RetroStarted { +- prompt: None, ++ prompt: None, + provider: None, +- model: None, ++ model: None, + }), + "retro.started" + ); + assert_eq!( + event_name(&Event::ParallelBranchStarted { +- parallel_group_id: StageId::new("plan", 1), ++ parallel_group_id: StageId::new("plan", 1), + parallel_branch_id: ParallelBranchId::new(StageId::new("plan", 1), 0), +- branch: "fork".to_string(), +- index: 0, ++ branch: "fork".to_string(), ++ index: 0, + }), + "parallel.branch.started" + ); + assert_eq!( + event_name(&Event::Agent { +- stage: "code".to_string(), +- visit: 1, +- event: AgentEvent::SubAgentSpawned { ++ stage: "code".to_string(), ++ visit: 1, ++ event: AgentEvent::SubAgentSpawned { + agent_id: "a1".to_string(), +- depth: 1, +- task: "do it".to_string(), ++ depth: 1, ++ task: "do it".to_string(), + }, +- session_id: None, ++ session_id: None, + parent_session_id: None, + }), + "agent.sub.spawned" +@@ -3199,18 +3219,18 @@ mod tests { + let stored = to_run_event_at( + &fixtures::RUN_1, + &Event::StageStarted { +- node_id: "review".to_string(), +- name: "review".to_string(), +- index: 1, ++ node_id: "review".to_string(), ++ name: "review".to_string(), ++ index: 1, + handler_type: "agent".to_string(), +- attempt: 1, ++ attempt: 1, + max_attempts: 1, + }, + Utc::now(), + Some(&StageScope { +- node_id: "review".to_string(), +- visit: 1, +- parallel_group_id: Some(StageId::new("fanout", 2)), ++ node_id: "review".to_string(), ++ visit: 1, ++ parallel_group_id: Some(StageId::new("fanout", 2)), + parallel_branch_id: Some(ParallelBranchId::new(StageId::new("fanout", 2), 1)), + }), + ); +@@ -3223,24 +3243,30 @@ mod tests { + + #[test] + fn parallel_started_populates_parallel_group_id() { +- let stored = to_run_event(&fixtures::RUN_1, &Event::ParallelStarted { +- node_id: "fanout".to_string(), +- visit: 2, +- branch_count: 3, +- join_policy: "wait_all".to_string(), +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_1, ++ &Event::ParallelStarted { ++ node_id: "fanout".to_string(), ++ visit: 2, ++ branch_count: 3, ++ join_policy: "wait_all".to_string(), ++ }, ++ ); + assert_eq!(stored.parallel_group_id, Some(StageId::new("fanout", 2))); + assert!(stored.parallel_branch_id.is_none()); + } + + #[test] + fn parallel_branch_started_populates_group_and_branch_ids() { +- let stored = to_run_event(&fixtures::RUN_1, &Event::ParallelBranchStarted { +- parallel_group_id: StageId::new("fanout", 2), +- parallel_branch_id: ParallelBranchId::new(StageId::new("fanout", 2), 1), +- branch: "review".to_string(), +- index: 1, +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_1, ++ &Event::ParallelBranchStarted { ++ parallel_group_id: StageId::new("fanout", 2), ++ parallel_branch_id: ParallelBranchId::new(StageId::new("fanout", 2), 1), ++ branch: "review".to_string(), ++ index: 1, ++ }, ++ ); + assert_eq!(stored.parallel_group_id, Some(StageId::new("fanout", 2))); + assert_eq!( + stored.parallel_branch_id, +@@ -3253,21 +3279,21 @@ mod tests { + let stored = to_run_event_at( + &fixtures::RUN_1, + &Event::Agent { +- stage: "code".to_string(), +- visit: 3, +- event: AgentEvent::ToolCallStarted { +- tool_name: "read_file".to_string(), ++ stage: "code".to_string(), ++ visit: 3, ++ event: AgentEvent::ToolCallStarted { ++ tool_name: "read_file".to_string(), + tool_call_id: "call_abc".to_string(), +- arguments: serde_json::json!({"path": "src/main.rs"}), ++ arguments: serde_json::json!({"path": "src/main.rs"}), + }, +- session_id: Some("ses_1".to_string()), ++ session_id: Some("ses_1".to_string()), + parent_session_id: None, + }, + Utc::now(), + Some(&StageScope { +- node_id: "code".to_string(), +- visit: 3, +- parallel_group_id: Some(StageId::new("fanout", 2)), ++ node_id: "code".to_string(), ++ visit: 3, ++ parallel_group_id: Some(StageId::new("fanout", 2)), + parallel_branch_id: Some(ParallelBranchId::new(StageId::new("fanout", 2), 0)), + }), + ); +@@ -3287,19 +3313,19 @@ mod tests { + // Prompt, InterviewStarted, Failover, GitCommit) should pick up stage_id + // / parallel_group_id / parallel_branch_id from the scope argument. + let scope = StageScope { +- node_id: "build".to_string(), +- visit: 2, +- parallel_group_id: Some(StageId::new("fanout", 1)), ++ node_id: "build".to_string(), ++ visit: 2, ++ parallel_group_id: Some(StageId::new("fanout", 1)), + parallel_branch_id: Some(ParallelBranchId::new(StageId::new("fanout", 1), 0)), + }; + + let command_started = to_run_event_at( + &fixtures::RUN_1, + &Event::CommandStarted { +- node_id: "build".to_string(), +- script: "echo".to_string(), +- command: "echo".to_string(), +- language: "shell".to_string(), ++ node_id: "build".to_string(), ++ script: "echo".to_string(), ++ command: "echo".to_string(), ++ language: "shell".to_string(), + timeout_ms: None, + }, + Utc::now(), +@@ -3312,12 +3338,12 @@ mod tests { + let prompt = to_run_event_at( + &fixtures::RUN_1, + &Event::Prompt { +- stage: "build".to_string(), +- visit: 2, +- text: "do it".to_string(), +- mode: None, ++ stage: "build".to_string(), ++ visit: 2, ++ text: "do it".to_string(), ++ mode: None, + provider: None, +- model: None, ++ model: None, + }, + Utc::now(), + Some(&scope), +@@ -3328,7 +3354,7 @@ mod tests { + &fixtures::RUN_1, + &Event::GitCommit { + node_id: Some("build".to_string()), +- sha: "deadbeef".to_string(), ++ sha: "deadbeef".to_string(), + }, + Utc::now(), + Some(&scope), +@@ -3347,42 +3373,52 @@ mod tests { + #[test] + fn control_action_events_carry_actor_in_envelope() { + let actor = ActorRef { +- kind: ActorKind::User, +- id: Some("alice".to_string()), ++ kind: ActorKind::User, ++ id: Some("alice".to_string()), + display: Some("alice".to_string()), + }; + +- let cancel = to_run_event(&fixtures::RUN_1, &Event::RunCancelRequested { +- actor: Some(actor.clone()), +- }); ++ let cancel = to_run_event( ++ &fixtures::RUN_1, ++ &Event::RunCancelRequested { ++ actor: Some(actor.clone()), ++ }, ++ ); + assert_eq!(cancel.event_name(), "run.cancel.requested"); + assert_eq!(cancel.actor.as_ref().expect("actor set"), &actor); + +- let pause = to_run_event(&fixtures::RUN_1, &Event::RunPauseRequested { +- actor: Some(actor.clone()), +- }); ++ let pause = to_run_event( ++ &fixtures::RUN_1, ++ &Event::RunPauseRequested { ++ actor: Some(actor.clone()), ++ }, ++ ); + assert_eq!(pause.actor.as_ref().expect("actor set"), &actor); + +- let unpause = to_run_event(&fixtures::RUN_1, &Event::RunUnpauseRequested { +- actor: None, +- }); ++ let unpause = to_run_event( ++ &fixtures::RUN_1, ++ &Event::RunUnpauseRequested { actor: None }, ++ ); + assert!(unpause.actor.is_none()); + } + + #[test] + fn agent_assistant_message_populates_agent_actor() { +- let stored = to_run_event(&fixtures::RUN_1, &Event::Agent { +- stage: "code".to_string(), +- visit: 1, +- event: AgentEvent::AssistantMessage { +- text: "ok".to_string(), +- model: "claude-sonnet".to_string(), +- usage: LlmTokenCounts::default(), +- tool_call_count: 0, ++ let stored = to_run_event( ++ &fixtures::RUN_1, ++ &Event::Agent { ++ stage: "code".to_string(), ++ visit: 1, ++ event: AgentEvent::AssistantMessage { ++ text: "ok".to_string(), ++ model: "claude-sonnet".to_string(), ++ usage: LlmTokenCounts::default(), ++ tool_call_count: 0, ++ }, ++ session_id: Some("ses_agent".to_string()), ++ parent_session_id: None, + }, +- session_id: Some("ses_agent".to_string()), +- parent_session_id: None, +- }); ++ ); + let actor = stored.actor.as_ref().expect("actor set"); + assert_eq!(actor.kind, ActorKind::Agent); + assert_eq!(actor.id.as_deref(), Some("ses_agent")); +@@ -3395,31 +3431,34 @@ mod tests { + use ::fabro_types::{Graph, RunAuthMethod, RunSubjectProvenance, fixtures}; + + let provenance = RunProvenance { +- server: None, +- client: None, ++ server: None, ++ client: None, + subject: Some(RunSubjectProvenance { +- login: Some("alice".to_string()), ++ login: Some("alice".to_string()), + auth_method: RunAuthMethod::Github, + }), + }; + +- let stored = to_run_event(&fixtures::RUN_1, &Event::RunCreated { +- run_id: fixtures::RUN_1, +- settings: serde_json::to_value(SettingsLayer::default()).unwrap(), +- graph: serde_json::to_value(Graph::new("test")).unwrap(), +- workflow_source: None, +- workflow_config: None, +- labels: BTreeMap::default(), +- run_dir: "/tmp/run".to_string(), +- working_directory: "/tmp/run".to_string(), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- workflow_slug: None, +- db_prefix: None, +- provenance: Some(provenance), +- manifest_blob: None, +- }); ++ let stored = to_run_event( ++ &fixtures::RUN_1, ++ &Event::RunCreated { ++ run_id: fixtures::RUN_1, ++ settings: serde_json::to_value(SettingsLayer::default()).unwrap(), ++ graph: serde_json::to_value(Graph::new("test")).unwrap(), ++ workflow_source: None, ++ workflow_config: None, ++ labels: BTreeMap::default(), ++ run_dir: "/tmp/run".to_string(), ++ working_directory: "/tmp/run".to_string(), ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ workflow_slug: None, ++ db_prefix: None, ++ provenance: Some(provenance), ++ manifest_blob: None, ++ }, ++ ); + let actor = stored.actor.as_ref().expect("actor set"); + assert_eq!(actor.kind, ActorKind::User); + assert_eq!(actor.id.as_deref(), Some("alice")); +diff --git a/lib/crates/fabro-workflow/src/file_resolver.rs b/lib/crates/fabro-workflow/src/file_resolver.rs +index c53ec7c3..ec6afbe2 100644 +--- a/lib/crates/fabro-workflow/src/file_resolver.rs ++++ b/lib/crates/fabro-workflow/src/file_resolver.rs +@@ -8,7 +8,7 @@ pub trait FileResolver: Send + Sync { + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct ResolvedFile { + pub logical_path: PathBuf, +- pub content: String, ++ pub content: String, + } + + #[derive(Clone, Debug, Default)] +diff --git a/lib/crates/fabro-workflow/src/git.rs b/lib/crates/fabro-workflow/src/git.rs +index 60d4dc71..3b2b458e 100644 +--- a/lib/crates/fabro-workflow/src/git.rs ++++ b/lib/crates/fabro-workflow/src/git.rs +@@ -433,93 +433,121 @@ mod tests { + + let store = test_store(); + let run = store.create_run(&fixtures::RUN_1).await.unwrap(); +- append_event(&run, &fixtures::RUN_1, &Event::Prompt { +- stage: "work".into(), +- visit: 2, +- text: "hello".into(), +- mode: Some("prompt".into()), +- provider: Some("openai".into()), +- model: Some("gpt-5.4".into()), +- }) ++ append_event( ++ &run, ++ &fixtures::RUN_1, ++ &Event::Prompt { ++ stage: "work".into(), ++ visit: 2, ++ text: "hello".into(), ++ mode: Some("prompt".into()), ++ provider: Some("openai".into()), ++ model: Some("gpt-5.4".into()), ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &fixtures::RUN_1, &Event::PromptCompleted { +- node_id: "work".into(), +- response: "world".into(), +- model: "gpt-5.4".into(), +- provider: "openai".into(), +- billing: None, +- }) ++ append_event( ++ &run, ++ &fixtures::RUN_1, ++ &Event::PromptCompleted { ++ node_id: "work".into(), ++ response: "world".into(), ++ model: "gpt-5.4".into(), ++ provider: "openai".into(), ++ billing: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &fixtures::RUN_1, &Event::StageCompleted { +- node_id: "work".into(), +- name: "Work".into(), +- index: 2, +- duration_ms: 100, +- status: "success".into(), +- preferred_label: None, +- suggested_next_ids: Vec::new(), +- billing: None, +- failure: None, +- notes: None, +- files_touched: Vec::new(), +- context_updates: None, +- jump_to_node: None, +- context_values: None, +- node_visits: Some(std::collections::BTreeMap::from([("work".into(), 2)])), +- loop_failure_signatures: None, +- restart_failure_signatures: None, +- response: Some("world".into()), +- attempt: 1, +- max_attempts: 1, +- }) ++ append_event( ++ &run, ++ &fixtures::RUN_1, ++ &Event::StageCompleted { ++ node_id: "work".into(), ++ name: "Work".into(), ++ index: 2, ++ duration_ms: 100, ++ status: "success".into(), ++ preferred_label: None, ++ suggested_next_ids: Vec::new(), ++ billing: None, ++ failure: None, ++ notes: None, ++ files_touched: Vec::new(), ++ context_updates: None, ++ jump_to_node: None, ++ context_values: None, ++ node_visits: Some(std::collections::BTreeMap::from([("work".into(), 2)])), ++ loop_failure_signatures: None, ++ restart_failure_signatures: None, ++ response: Some("world".into()), ++ attempt: 1, ++ max_attempts: 1, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &fixtures::RUN_1, &Event::CommandStarted { +- node_id: "work".into(), +- script: "echo hi".into(), +- command: "echo hi".into(), +- language: "shell".into(), +- timeout_ms: None, +- }) ++ append_event( ++ &run, ++ &fixtures::RUN_1, ++ &Event::CommandStarted { ++ node_id: "work".into(), ++ script: "echo hi".into(), ++ command: "echo hi".into(), ++ language: "shell".into(), ++ timeout_ms: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &fixtures::RUN_1, &Event::CommandCompleted { +- node_id: "work".into(), +- stdout: "hi\n".into(), +- stderr: String::new(), +- exit_code: Some(0), +- duration_ms: 10, +- timed_out: false, +- }) ++ append_event( ++ &run, ++ &fixtures::RUN_1, ++ &Event::CommandCompleted { ++ node_id: "work".into(), ++ stdout: "hi\n".into(), ++ stderr: String::new(), ++ exit_code: Some(0), ++ duration_ms: 10, ++ timed_out: false, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &fixtures::RUN_1, &Event::ParallelCompleted { +- node_id: "work".into(), +- visit: 2, +- duration_ms: 100, +- success_count: 1, +- failure_count: 0, +- results: vec![serde_json::json!({"id": "a"})], +- }) ++ append_event( ++ &run, ++ &fixtures::RUN_1, ++ &Event::ParallelCompleted { ++ node_id: "work".into(), ++ visit: 2, ++ duration_ms: 100, ++ success_count: 1, ++ failure_count: 0, ++ results: vec![serde_json::json!({"id": "a"})], ++ }, ++ ) + .await + .unwrap(); +- append_event(&run, &fixtures::RUN_1, &Event::CheckpointCompleted { +- node_id: "work".into(), +- status: "success".into(), +- current_node: "work".into(), +- completed_nodes: Vec::new(), +- node_retries: std::collections::BTreeMap::new(), +- context_values: std::collections::BTreeMap::new(), +- node_outcomes: std::collections::BTreeMap::new(), +- next_node_id: None, +- git_commit_sha: None, +- loop_failure_signatures: std::collections::BTreeMap::new(), +- restart_failure_signatures: std::collections::BTreeMap::new(), +- node_visits: std::collections::BTreeMap::from([("work".into(), 2)]), +- diff: Some("diff --git a/story.txt b/story.txt".into()), +- }) ++ append_event( ++ &run, ++ &fixtures::RUN_1, ++ &Event::CheckpointCompleted { ++ node_id: "work".into(), ++ status: "success".into(), ++ current_node: "work".into(), ++ completed_nodes: Vec::new(), ++ node_retries: std::collections::BTreeMap::new(), ++ context_values: std::collections::BTreeMap::new(), ++ node_outcomes: std::collections::BTreeMap::new(), ++ next_node_id: None, ++ git_commit_sha: None, ++ loop_failure_signatures: std::collections::BTreeMap::new(), ++ restart_failure_signatures: std::collections::BTreeMap::new(), ++ node_visits: std::collections::BTreeMap::from([("work".into(), 2)]), ++ diff: Some("diff --git a/story.txt b/story.txt".into()), ++ }, ++ ) + .await + .unwrap(); + +diff --git a/lib/crates/fabro-workflow/src/graph.rs b/lib/crates/fabro-workflow/src/graph.rs +index b4eb1390..5ccfed63 100644 +--- a/lib/crates/fabro-workflow/src/graph.rs ++++ b/lib/crates/fabro-workflow/src/graph.rs +@@ -114,7 +114,7 @@ impl Graph for WorkflowGraph { + node.inner().selection(), + ); + selection.map(|sel| CoreEdgeSelection { +- edge: WorkflowEdge(Arc::new(sel.edge.clone())), ++ edge: WorkflowEdge(Arc::new(sel.edge.clone())), + reason: sel.reason, + }) + } +diff --git a/lib/crates/fabro-workflow/src/graph/routing.rs b/lib/crates/fabro-workflow/src/graph/routing.rs +index 58851bd8..a9621556 100644 +--- a/lib/crates/fabro-workflow/src/graph/routing.rs ++++ b/lib/crates/fabro-workflow/src/graph/routing.rs +@@ -9,7 +9,7 @@ use crate::outcome::{Outcome, StageStatus}; + + /// Result of edge selection: the chosen edge and the reason it was selected. + pub(crate) struct SelectedGraphEdge<'a> { +- pub(crate) edge: &'a GvEdge, ++ pub(crate) edge: &'a GvEdge, + pub(crate) reason: &'static str, + } + +diff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs +index 0c9008a7..a3e832cb 100644 +--- a/lib/crates/fabro-workflow/src/handler/agent.rs ++++ b/lib/crates/fabro-workflow/src/handler/agent.rs +@@ -19,9 +19,9 @@ use crate::outcome::{ + /// Result from a `CodergenBackend` invocation. + pub enum CodergenResult { + Text { +- text: String, +- usage: Option, +- files_touched: Vec, ++ text: String, ++ usage: Option, ++ files_touched: Vec, + last_file_touched: Option, + }, + Full(Outcome), +@@ -256,12 +256,12 @@ impl Handler for AgentHandler { + let stage_scope = StageScope::for_handler(context, &node.id); + services.emitter.emit_scoped( + &Event::Prompt { +- stage: node.id.clone(), +- visit: stage_scope.visit, +- text: prompt.clone(), +- mode: Some("agent".to_string()), ++ stage: node.id.clone(), ++ visit: stage_scope.visit, ++ text: prompt.clone(), ++ mode: Some("agent".to_string()), + provider: prompt_provider, +- model: prompt_model, ++ model: prompt_model, + }, + &stage_scope, + ); +@@ -332,11 +332,11 @@ impl Handler for AgentHandler { + .unwrap_or_default(); + services.emitter.emit_scoped( + &Event::PromptCompleted { +- node_id: node.id.clone(), ++ node_id: node.id.clone(), + response: response_text.clone(), +- model: response_model, ++ model: response_model, + provider: response_provider, +- billing: stage_usage.clone(), ++ billing: stage_usage.clone(), + }, + &stage_scope, + ); +@@ -603,11 +603,10 @@ mod tests { + _tool_hooks: Option>, + ) -> Result { + Ok(CodergenResult::Text { +- text: +- r#"Done. {"outcome": "success", "preferred_next_label": "approve"}"# +- .to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: r#"Done. {"outcome": "success", "preferred_next_label": "approve"}"# ++ .to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -660,9 +659,9 @@ mod tests { + _tool_hooks: Option>, + ) -> Result { + Ok(CodergenResult::Text { +- text: "Done writing results.".to_string(), +- usage: None, +- files_touched: vec!["results.md".to_string()], ++ text: "Done writing results.".to_string(), ++ usage: None, ++ files_touched: vec!["results.md".to_string()], + last_file_touched: Some("results.md".to_string()), + }) + } +@@ -720,21 +719,21 @@ mod tests { + let scope = StageScope::for_handler(context, &node.id); + emitter.emit_scoped( + &crate::event::Event::Agent { +- stage: node.id.clone(), +- visit: scope.visit, +- event: fabro_agent::AgentEvent::SessionStarted { ++ stage: node.id.clone(), ++ visit: scope.visit, ++ event: fabro_agent::AgentEvent::SessionStarted { + provider: Some("openai".to_string()), +- model: Some("gpt-5.4".to_string()), ++ model: Some("gpt-5.4".to_string()), + }, +- session_id: Some("session_123".to_string()), ++ session_id: Some("session_123".to_string()), + parent_session_id: None, + }, + &scope, + ); + Ok(CodergenResult::Text { +- text: "done".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "done".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -830,9 +829,9 @@ mod tests { + ) -> Result { + *self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from)); + Ok(CodergenResult::Text { +- text: "ok".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "ok".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -882,9 +881,9 @@ mod tests { + ) -> Result { + *self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from)); + Ok(CodergenResult::Text { +- text: "ok".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "ok".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -1110,9 +1109,9 @@ Some text in between. + ) -> Result { + *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); + Ok(CodergenResult::Text { +- text: "ok".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "ok".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -1179,9 +1178,9 @@ Some text in between. + ) -> Result { + *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); + Ok(CodergenResult::Text { +- text: "ok".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "ok".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +diff --git a/lib/crates/fabro-workflow/src/handler/command.rs b/lib/crates/fabro-workflow/src/handler/command.rs +index c88e43b6..766a9e3e 100644 +--- a/lib/crates/fabro-workflow/src/handler/command.rs ++++ b/lib/crates/fabro-workflow/src/handler/command.rs +@@ -92,10 +92,10 @@ impl Handler for CommandHandler { + let stage_scope = StageScope::for_handler(context, &node.id); + services.emitter.emit_scoped( + &Event::CommandStarted { +- node_id: node.id.clone(), +- script: script.to_string(), +- command: command.clone(), +- language: language.to_string(), ++ node_id: node.id.clone(), ++ script: script.to_string(), ++ command: command.clone(), ++ language: language.to_string(), + timeout_ms: timeout_ms(node), + }, + &stage_scope, +@@ -122,12 +122,12 @@ impl Handler for CommandHandler { + + services.emitter.emit_scoped( + &Event::CommandCompleted { +- node_id: node.id.clone(), +- stdout: result.stdout.clone(), +- stderr: result.stderr.clone(), +- exit_code: (!result.timed_out).then_some(result.exit_code), ++ node_id: node.id.clone(), ++ stdout: result.stdout.clone(), ++ stderr: result.stderr.clone(), ++ exit_code: (!result.timed_out).then_some(result.exit_code), + duration_ms: result.duration_ms, +- timed_out: result.timed_out, ++ timed_out: result.timed_out, + }, + &stage_scope, + ); +@@ -713,9 +713,9 @@ mod tests { + /// proving that `CommandHandler` delegates to the sandbox rather than + /// spawning a host process. + struct SpySandbox { +- exec_result: fabro_agent::sandbox::ExecResult, +- captured_command: std::sync::Mutex>, +- captured_env_vars: std::sync::Mutex>>, ++ exec_result: fabro_agent::sandbox::ExecResult, ++ captured_command: std::sync::Mutex>, ++ captured_env_vars: std::sync::Mutex>>, + captured_cancel_token: std::sync::Mutex>, + } + +@@ -816,10 +816,10 @@ mod tests { + #[tokio::test] + async fn executes_script_via_sandbox() { + let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { +- stdout: "SANDBOX_MARKER\n".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "SANDBOX_MARKER\n".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 5, + })); + +@@ -861,10 +861,10 @@ mod tests { + #[tokio::test] + async fn executes_python_script_via_sandbox() { + let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { +- stdout: "PYTHON_SANDBOX\n".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "PYTHON_SANDBOX\n".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 5, + })); + +@@ -904,10 +904,10 @@ mod tests { + #[tokio::test] + async fn passes_env_vars_to_sandbox() { + let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { +- stdout: String::new(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: String::new(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 5, + })); + +@@ -939,10 +939,10 @@ mod tests { + #[tokio::test] + async fn passes_run_cancellation_to_sandbox() { + let spy = std::sync::Arc::new(SpySandbox::new(fabro_agent::sandbox::ExecResult { +- stdout: String::new(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: String::new(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 5, + })); + +diff --git a/lib/crates/fabro-workflow/src/handler/fan_in.rs b/lib/crates/fabro-workflow/src/handler/fan_in.rs +index 3c411140..1689620c 100644 +--- a/lib/crates/fabro-workflow/src/handler/fan_in.rs ++++ b/lib/crates/fabro-workflow/src/handler/fan_in.rs +@@ -141,9 +141,9 @@ impl Handler for FanInHandler { + } + + struct Candidate { +- id: String, ++ id: String, + status: String, +- score: f64, ++ score: f64, + } + + fn status_rank(status: &str) -> u32 { +@@ -161,16 +161,16 @@ fn heuristic_select(results: &serde_json::Value) -> Candidate { + let arr = results.as_array().unwrap_or(&empty_vec); + if arr.is_empty() { + return Candidate { +- id: "unknown".to_string(), ++ id: "unknown".to_string(), + status: "fail".to_string(), +- score: 0.0, ++ score: 0.0, + }; + } + + let mut candidates: Vec = arr + .iter() + .map(|v| Candidate { +- id: v ++ id: v + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") +@@ -180,7 +180,7 @@ fn heuristic_select(results: &serde_json::Value) -> Candidate { + .and_then(|v| v.as_str()) + .unwrap_or("fail") + .to_string(), +- score: v ++ score: v + .get("score") + .and_then(serde_json::Value::as_f64) + .unwrap_or(0.0), +@@ -204,9 +204,9 @@ fn heuristic_select(results: &serde_json::Value) -> Candidate { + }); + + candidates.into_iter().next().unwrap_or_else(|| Candidate { +- id: "unknown".to_string(), ++ id: "unknown".to_string(), + status: "fail".to_string(), +- score: 0.0, ++ score: 0.0, + }) + } + +@@ -234,12 +234,12 @@ async fn llm_evaluate( + + emitter.emit_scoped( + &Event::Prompt { +- stage: node_id.to_string(), +- visit: stage_scope.visit, +- text: full_prompt.clone(), +- mode: Some("fan_in".to_string()), ++ stage: node_id.to_string(), ++ visit: stage_scope.visit, ++ text: full_prompt.clone(), ++ mode: Some("fan_in".to_string()), + provider: None, +- model: None, ++ model: None, + }, + &stage_scope, + ); +@@ -273,28 +273,28 @@ async fn llm_evaluate( + serde_json::to_string_pretty(&outcome).unwrap_or_else(|_| "{}".to_string()); + emitter.emit_scoped( + &Event::PromptCompleted { +- node_id: node_id.to_string(), ++ node_id: node_id.to_string(), + response: response_text.clone(), +- model: String::new(), ++ model: String::new(), + provider: String::new(), +- billing: None, ++ billing: None, + }, + &stage_scope, + ); + Ok(Candidate { +- id: best_id, ++ id: best_id, + status: outcome.status.to_string(), +- score: 0.0, ++ score: 0.0, + }) + } + Ok(CodergenResult::Text { text, .. }) => { + emitter.emit_scoped( + &Event::PromptCompleted { +- node_id: node_id.to_string(), ++ node_id: node_id.to_string(), + response: text.clone(), +- model: String::new(), ++ model: String::new(), + provider: String::new(), +- billing: None, ++ billing: None, + }, + &stage_scope, + ); +@@ -473,9 +473,9 @@ mod tests { + ) -> Result { + // Return text that contains the ID "branch_b" + Ok(CodergenResult::Text { +- text: "The best candidate is branch_b".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "The best candidate is branch_b".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +diff --git a/lib/crates/fabro-workflow/src/handler/human.rs b/lib/crates/fabro-workflow/src/handler/human.rs +index 9dbeeaf8..d0491a25 100644 +--- a/lib/crates/fabro-workflow/src/handler/human.rs ++++ b/lib/crates/fabro-workflow/src/handler/human.rs +@@ -18,9 +18,9 @@ use crate::outcome::{Outcome, OutcomeExt}; + + /// A choice derived from an outgoing edge. + struct Choice { +- key: String, ++ key: String, + label: String, +- to: String, ++ to: String, + } + + /// Parse an accelerator key from a label. +@@ -69,7 +69,7 @@ fn parse_accelerator_key(label: &str) -> String { + /// Blocks until a human selects an option derived from outgoing edges. + pub struct HumanHandler { + interviewer: Arc, +- emitter: Option>, ++ emitter: Option>, + } + + impl HumanHandler { +@@ -176,7 +176,7 @@ impl Handler for HumanHandler { + let options: Vec = choices + .iter() + .map(|c| QuestionOption { +- key: c.key.clone(), ++ key: c.key.clone(), + label: c.label.clone(), + }) + .collect(); +@@ -211,19 +211,19 @@ impl Handler for HumanHandler { + self.emit( + &services.emitter, + &Event::InterviewStarted { +- question_id: question_id.clone(), +- question: question_text.clone(), +- stage: node.id.clone(), +- question_type: question.question_type.to_string(), +- options: question ++ question_id: question_id.clone(), ++ question: question_text.clone(), ++ stage: node.id.clone(), ++ question_type: question.question_type.to_string(), ++ options: question + .options + .iter() + .map(|option| InterviewOption { +- key: option.key.clone(), ++ key: option.key.clone(), + label: option.label.clone(), + }) + .collect(), +- allow_freeform: question.allow_freeform, ++ allow_freeform: question.allow_freeform, + timeout_seconds: question.timeout_seconds, + context_display: question.context_display.clone(), + }, +@@ -238,8 +238,8 @@ impl Handler for HumanHandler { + &services.emitter, + &Event::InterviewTimeout { + question_id: question_id.clone(), +- question: question_text, +- stage: node.id.clone(), ++ question: question_text, ++ stage: node.id.clone(), + duration_ms: millis_u64(interview_start.elapsed()), + }, + &stage_scope, +@@ -275,9 +275,9 @@ impl Handler for HumanHandler { + &services.emitter, + &Event::InterviewInterrupted { + question_id: question_id.clone(), +- question: question_text, +- stage: node.id.clone(), +- reason: "interrupted".to_string(), ++ question: question_text, ++ stage: node.id.clone(), ++ reason: "interrupted".to_string(), + duration_ms: millis_u64(interview_start.elapsed()), + }, + &stage_scope, +diff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs +index 27ad86ae..bd9049a8 100644 +--- a/lib/crates/fabro-workflow/src/handler/llm/api.rs ++++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs +@@ -36,7 +36,7 @@ fn build_profile(model: &str, provider: Provider) -> Box { + } + + pub(crate) struct LlmClientBuildResult { +- pub(crate) client: Client, ++ pub(crate) client: Client, + pub(crate) auth_issues: Vec<(Provider, ResolveError)>, + } + +@@ -101,7 +101,7 @@ struct FileTracking { + /// Set of all file paths successfully written/edited. + touched: HashSet, + /// Most recently modified file path. +- last: Option, ++ last: Option, + } + + fn track_file_event(event: &AgentEvent, state: &mut FileTracking) { +@@ -158,10 +158,10 @@ fn spawn_event_forwarder( + { + emitter.emit_scoped( + &Event::Agent { +- stage: node_id.clone(), +- visit: scope.visit, +- event: event.event.clone(), +- session_id: Some(event.session_id.clone()), ++ stage: node_id.clone(), ++ visit: scope.visit, ++ event: event.event.clone(), ++ session_id: Some(event.session_id.clone()), + parent_session_id: event.parent_session_id.clone(), + }, + &scope, +@@ -176,13 +176,13 @@ fn spawn_event_forwarder( + /// For `full` fidelity nodes sharing a thread key, sessions are cached + /// and reused so the LLM sees the full conversation history. + pub struct AgentApiBackend { +- model: String, +- provider: Provider, ++ model: String, ++ provider: Provider, + fallback_chain: Vec, +- sessions: Mutex>, +- env: HashMap, +- mcp_servers: Vec, +- resolver: Option, ++ sessions: Mutex>, ++ env: HashMap, ++ mcp_servers: Vec, ++ resolver: Option, + } + + impl AgentApiBackend { +@@ -470,9 +470,9 @@ impl CodergenBackend for AgentApiBackend { + ); + + Ok(CodergenResult::Text { +- text: response.text(), +- usage: Some(stage_usage), +- files_touched: Vec::new(), ++ text: response.text(), ++ usage: Some(stage_usage), ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -531,7 +531,7 @@ impl CodergenBackend for AgentApiBackend { + let file_tracking = Arc::new(Mutex::new(FileTracking { + pending: HashMap::new(), + touched: HashSet::new(), +- last: None, ++ last: None, + })); + let stage_scope = StageScope::for_handler(context, &node.id); + +@@ -569,12 +569,12 @@ impl CodergenBackend for AgentApiBackend { + for target in &self.fallback_chain { + emitter.emit_scoped( + &Event::Failover { +- stage: node.id.clone(), ++ stage: node.id.clone(), + from_provider: from_provider.clone(), +- from_model: from_model.clone(), +- to_provider: target.provider.clone(), +- to_model: target.model.clone(), +- error: error_msg.clone(), ++ from_model: from_model.clone(), ++ to_provider: target.provider.clone(), ++ to_model: target.model.clone(), ++ error: error_msg.clone(), + }, + &stage_scope, + ); +@@ -730,7 +730,7 @@ mod tests { + FileTracking { + pending: HashMap::new(), + touched: HashSet::new(), +- last: None, ++ last: None, + } + } + +@@ -746,9 +746,9 @@ mod tests { + + track_file_event( + &AgentEvent::ToolCallStarted { +- tool_name: "write_file".to_string(), ++ tool_name: "write_file".to_string(), + tool_call_id: "tc1".to_string(), +- arguments: serde_json::Value::Object(args), ++ arguments: serde_json::Value::Object(args), + }, + &mut state, + ); +@@ -757,9 +757,9 @@ mod tests { + track_file_event( + &AgentEvent::ToolCallCompleted { + tool_call_id: "tc1".to_string(), +- tool_name: "write_file".to_string(), +- is_error: false, +- output: serde_json::Value::String("ok".to_string()), ++ tool_name: "write_file".to_string(), ++ is_error: false, ++ output: serde_json::Value::String("ok".to_string()), + }, + &mut state, + ); +@@ -779,9 +779,9 @@ mod tests { + + track_file_event( + &AgentEvent::ToolCallStarted { +- tool_name: "edit_file".to_string(), ++ tool_name: "edit_file".to_string(), + tool_call_id: "tc-sub".to_string(), +- arguments: serde_json::Value::Object(args), ++ arguments: serde_json::Value::Object(args), + }, + &mut state, + ); +@@ -790,9 +790,9 @@ mod tests { + track_file_event( + &AgentEvent::ToolCallCompleted { + tool_call_id: "tc-sub".to_string(), +- tool_name: "edit_file".to_string(), +- is_error: false, +- output: serde_json::Value::String("ok".to_string()), ++ tool_name: "edit_file".to_string(), ++ is_error: false, ++ output: serde_json::Value::String("ok".to_string()), + }, + &mut state, + ); +@@ -812,9 +812,9 @@ mod tests { + + track_file_event( + &AgentEvent::ToolCallStarted { +- tool_name: "edit_file".to_string(), ++ tool_name: "edit_file".to_string(), + tool_call_id: "tc-err".to_string(), +- arguments: serde_json::Value::Object(args), ++ arguments: serde_json::Value::Object(args), + }, + &mut state, + ); +@@ -822,9 +822,9 @@ mod tests { + track_file_event( + &AgentEvent::ToolCallCompleted { + tool_call_id: "tc-err".to_string(), +- tool_name: "edit_file".to_string(), +- is_error: true, +- output: serde_json::Value::String("failed".to_string()), ++ tool_name: "edit_file".to_string(), ++ is_error: true, ++ output: serde_json::Value::String("failed".to_string()), + }, + &mut state, + ); +@@ -857,7 +857,7 @@ mod tests { + "anthropic", + &serde_json::to_string(&AuthCredential { + provider: Provider::Anthropic, +- details: AuthDetails::ApiKey { ++ details: AuthDetails::ApiKey { + key: "anthropic-key".to_string(), + }, + }) +diff --git a/lib/crates/fabro-workflow/src/handler/llm/cli.rs b/lib/crates/fabro-workflow/src/handler/llm/cli.rs +index a34c15d2..dbb4cd35 100644 +--- a/lib/crates/fabro-workflow/src/handler/llm/cli.rs ++++ b/lib/crates/fabro-workflow/src/handler/llm/cli.rs +@@ -211,8 +211,8 @@ pub fn cli_command_for_provider(provider: Provider, model: &str, prompt_file: &s + /// Parsed response from a CLI tool invocation. + #[derive(Debug)] + pub struct CliResponse { +- pub text: String, +- pub input_tokens: i64, ++ pub text: String, ++ pub input_tokens: i64, + pub output_tokens: i64, + } + +@@ -380,11 +380,11 @@ fn shell_quote(val: &str) -> String { + /// CLI backend that invokes external CLI tools (claude, codex, gemini) via + /// `exec_command()`. + pub struct AgentCliBackend { +- model: String, +- provider: Provider, +- env: HashMap, ++ model: String, ++ provider: Provider, ++ env: HashMap, + poll_interval: std::time::Duration, +- resolver: Option, ++ resolver: Option, + } + + impl AgentCliBackend { +@@ -517,12 +517,12 @@ impl CodergenBackend for AgentCliBackend { + let stage_scope = StageScope::for_handler(context, &node.id); + emitter.emit_scoped( + &Event::AgentCliStarted { +- node_id: node.id.clone(), +- visit: stage_scope.visit, +- mode: "cli".to_string(), ++ node_id: node.id.clone(), ++ visit: stage_scope.visit, ++ mode: "cli".to_string(), + provider: provider.as_str().to_string(), +- model: model.to_string(), +- command: command.clone(), ++ model: model.to_string(), ++ command: command.clone(), + }, + &stage_scope, + ); +@@ -654,10 +654,10 @@ impl CodergenBackend for AgentCliBackend { + }; + emitter.emit_scoped( + &Event::AgentCliCompleted { +- node_id: node.id.clone(), +- stdout: result.stdout.clone(), +- stderr: result.stderr.clone(), +- exit_code: result.exit_code, ++ node_id: node.id.clone(), ++ stdout: result.stdout.clone(), ++ stderr: result.stderr.clone(), ++ exit_code: result.exit_code, + duration_ms: result.duration_ms, + }, + &stage_scope, +@@ -724,12 +724,16 @@ impl CodergenBackend for AgentCliBackend { + } + }; + +- let stage_usage = +- billed_model_usage_from_llm(model, provider, node.speed(), &TokenCounts { ++ let stage_usage = billed_model_usage_from_llm( ++ model, ++ provider, ++ node.speed(), ++ &TokenCounts { + input_tokens: parsed.input_tokens, + output_tokens: parsed.output_tokens, + ..TokenCounts::default() +- }); ++ }, ++ ); + + Ok(CodergenResult::Text { + text: parsed.text, +@@ -859,7 +863,7 @@ mod tests { + + /// Mock sandbox that returns pre-configured ExecResults in FIFO order. + struct CliMockSandbox { +- results: Mutex>, ++ results: Mutex>, + commands: Arc>>, + } + +@@ -952,20 +956,20 @@ mod tests { + + fn ok_result() -> ExecResult { + ExecResult { +- exit_code: 0, +- stdout: String::new(), +- stderr: String::new(), +- timed_out: false, ++ exit_code: 0, ++ stdout: String::new(), ++ stderr: String::new(), ++ timed_out: false, + duration_ms: 10, + } + } + + fn fail_result(code: i32) -> ExecResult { + ExecResult { +- exit_code: code, +- stdout: String::new(), +- stderr: "error".to_string(), +- timed_out: false, ++ exit_code: code, ++ stdout: String::new(), ++ stderr: "error".to_string(), ++ timed_out: false, + duration_ms: 10, + } + } +@@ -1229,9 +1233,9 @@ mod tests { + _tool_hooks: Option>, + ) -> Result { + Ok(CodergenResult::Text { +- text: "stub".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "stub".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +diff --git a/lib/crates/fabro-workflow/src/handler/llm/preamble.rs b/lib/crates/fabro-workflow/src/handler/llm/preamble.rs +index b8d8ab7d..e985cb80 100644 +--- a/lib/crates/fabro-workflow/src/handler/llm/preamble.rs ++++ b/lib/crates/fabro-workflow/src/handler/llm/preamble.rs +@@ -624,11 +624,16 @@ mod tests { + use crate::outcome::{BilledModelUsage, billed_model_usage_from_llm}; + + fn stage_usage(model: &str, input_tokens: i64, output_tokens: i64) -> BilledModelUsage { +- billed_model_usage_from_llm(model, Provider::Anthropic, None, &TokenCounts { +- input_tokens, +- output_tokens, +- ..TokenCounts::default() +- }) ++ billed_model_usage_from_llm( ++ model, ++ Provider::Anthropic, ++ None, ++ &TokenCounts { ++ input_tokens, ++ output_tokens, ++ ..TokenCounts::default() ++ }, ++ ) + } + + // --- truncate mode --- +diff --git a/lib/crates/fabro-workflow/src/handler/manager_loop.rs b/lib/crates/fabro-workflow/src/handler/manager_loop.rs +index 87150a7f..66968e89 100644 +--- a/lib/crates/fabro-workflow/src/handler/manager_loop.rs ++++ b/lib/crates/fabro-workflow/src/handler/manager_loop.rs +@@ -28,7 +28,7 @@ use crate::run_options::RunOptions; + pub struct SubWorkflowHandler; + + struct ParsedChildWorkflow { +- graph: Graph, ++ graph: Graph, + workflow_path: Option, + } + +@@ -68,12 +68,12 @@ fn parse_child_graph(node: &Node, services: &EngineServices) -> Result, +- pub emitter: Arc, +- pub sandbox: Arc, +- pub run_store: RunStoreHandle, ++ pub registry: Arc, ++ pub emitter: Arc, ++ pub sandbox: Arc, ++ pub run_store: RunStoreHandle, + /// Git state for the current run. Set via `set_git_state` at the start of + /// `run_via_core` and read by parallel/fan-in handlers. + pub(crate) git_state: std::sync::RwLock>>, + /// Hook runner for user-defined lifecycle hooks. +- pub hook_runner: Option>, ++ pub hook_runner: Option>, + /// Environment variables from `[sandbox.env]` config, injected into command + /// nodes. +- pub env: HashMap, ++ pub env: HashMap, + /// Typed values from `[run.inputs]`, available to prompt templates. +- pub inputs: HashMap, ++ pub inputs: HashMap, + /// When true, handlers should skip real execution and return simulated + /// results. +- pub dry_run: bool, ++ pub dry_run: bool, + /// Optional run-scoped cancellation flag from the core executor. + pub cancel_requested: Option>, + /// Resolved default provider for the current run. +- pub provider: Provider, ++ pub provider: Provider, + /// Logical path of the current workflow when running from a bundle. +- pub workflow_path: Option, ++ pub workflow_path: Option, + /// Bundled workflows available for child-workflow resolution. +- pub workflow_bundle: Option>, ++ pub workflow_bundle: Option>, + } + + impl EngineServices { +@@ -108,14 +108,14 @@ impl EngineServices { + None, + )); + Self { +- registry: Arc::new(HandlerRegistry::new(Box::new(start::StartHandler))), +- emitter: Arc::new(Emitter::default()), +- sandbox: Arc::new(fabro_agent::LocalSandbox::new( ++ registry: Arc::new(HandlerRegistry::new(Box::new(start::StartHandler))), ++ emitter: Arc::new(Emitter::default()), ++ sandbox: Arc::new(fabro_agent::LocalSandbox::new( + std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), + )), + // Build the test run store on a dedicated runtime so this helper + // remains safe to call from both sync tests and #[tokio::test]. +- run_store: std::thread::spawn(move || { ++ run_store: std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() +@@ -130,15 +130,15 @@ impl EngineServices { + .join() + .expect("test run store thread should join") + .into(), +- git_state: std::sync::RwLock::new(None), +- hook_runner: None, +- env: HashMap::new(), +- inputs: HashMap::new(), +- dry_run: false, ++ git_state: std::sync::RwLock::new(None), ++ hook_runner: None, ++ env: HashMap::new(), ++ inputs: HashMap::new(), ++ dry_run: false, + cancel_requested: None, +- provider: Provider::Anthropic, +- workflow_path: None, +- workflow_bundle: None, ++ provider: Provider::Anthropic, ++ workflow_path: None, ++ workflow_bundle: None, + } + } + } +@@ -237,7 +237,7 @@ pub async fn dispatch_handler( + + /// Maps handler type strings to handler implementations. + pub struct HandlerRegistry { +- handlers: HashMap>, ++ handlers: HashMap>, + default_handler: Box, + } + +diff --git a/lib/crates/fabro-workflow/src/handler/parallel.rs b/lib/crates/fabro-workflow/src/handler/parallel.rs +index 8b1fec8c..87afb627 100644 +--- a/lib/crates/fabro-workflow/src/handler/parallel.rs ++++ b/lib/crates/fabro-workflow/src/handler/parallel.rs +@@ -48,9 +48,9 @@ fn parse_join_policy(raw: &str) -> JoinPolicy { + } + + struct BranchResult { +- id: String, +- outcome: Outcome, +- head_sha: Option, ++ id: String, ++ outcome: Outcome, ++ head_sha: Option, + worktree_path: Option, + } + +@@ -128,12 +128,12 @@ impl Handler for ParallelHandler { + ) -> Result { + // Build per-branch sandboxes (sequentially for git setup) + struct BranchSetup { +- target_id: String, +- branch_index: usize, ++ target_id: String, ++ branch_index: usize, + parallel_branch_id: ParallelBranchId, +- branch_context: Context, +- sandbox: Arc, +- worktree_path: Option, ++ branch_context: Context, ++ sandbox: Arc, ++ worktree_path: Option, + } + + let parallel_start = Instant::now(); +@@ -154,10 +154,10 @@ impl Handler for ParallelHandler { + + services.emitter.emit_scoped( + &Event::ParallelStarted { +- node_id: node.id.clone(), +- visit: parallel_stage_scope.visit, ++ node_id: node.id.clone(), ++ visit: parallel_stage_scope.visit, + branch_count: branches.len(), +- join_policy: join_policy.to_string(), ++ join_policy: join_policy.to_string(), + }, + ¶llel_stage_scope, + ); +@@ -249,9 +249,9 @@ impl Handler for ParallelHandler { + + // Set up worktree via WorktreeSandbox + let wt_config = WorktreeOptions { +- branch_name: branch_name.clone(), +- base_sha: bsha.clone(), +- worktree_path: wt_path_str.clone(), ++ branch_name: branch_name.clone(), ++ base_sha: bsha.clone(), ++ worktree_path: wt_path_str.clone(), + skip_branch_creation: false, + }; + let mut wt_sandbox = WorktreeSandbox::new(Arc::clone(&services.sandbox), wt_config); +@@ -319,10 +319,10 @@ impl Handler for ParallelHandler { + + emitter.emit_scoped( + &Event::ParallelBranchStarted { +- parallel_group_id: group_id.clone(), ++ parallel_group_id: group_id.clone(), + parallel_branch_id: setup.parallel_branch_id.clone(), +- branch: setup.target_id.clone(), +- index: setup.branch_index, ++ branch: setup.target_id.clone(), ++ index: setup.branch_index, + }, + &branch_scope, + ); +@@ -335,13 +335,13 @@ impl Handler for ParallelHandler { + )); + emitter.emit_scoped( + &Event::ParallelBranchCompleted { +- parallel_group_id: group_id.clone(), ++ parallel_group_id: group_id.clone(), + parallel_branch_id: setup.parallel_branch_id.clone(), +- branch: setup.target_id.clone(), +- index: setup.branch_index, +- duration_ms: millis_u64(branch_start.elapsed()), +- status: "fail".to_string(), +- head_sha: None, ++ branch: setup.target_id.clone(), ++ index: setup.branch_index, ++ duration_ms: millis_u64(branch_start.elapsed()), ++ status: "fail".to_string(), ++ head_sha: None, + }, + &branch_scope, + ); +@@ -415,7 +415,7 @@ impl Handler for ParallelHandler { + emitter.emit_scoped( + &Event::GitCommit { + node_id: Some(setup.target_id.clone()), +- sha: sha.clone(), ++ sha: sha.clone(), + }, + &branch_scope, + ); +@@ -429,13 +429,13 @@ impl Handler for ParallelHandler { + + emitter.emit_scoped( + &Event::ParallelBranchCompleted { +- parallel_group_id: group_id.clone(), ++ parallel_group_id: group_id.clone(), + parallel_branch_id: setup.parallel_branch_id.clone(), +- branch: setup.target_id.clone(), +- index: setup.branch_index, +- duration_ms: millis_u64(branch_start.elapsed()), +- status: outcome.status.to_string(), +- head_sha: head_sha.clone(), ++ branch: setup.target_id.clone(), ++ index: setup.branch_index, ++ duration_ms: millis_u64(branch_start.elapsed()), ++ status: outcome.status.to_string(), ++ head_sha: head_sha.clone(), + }, + &branch_scope, + ); +@@ -459,19 +459,17 @@ impl Handler for ParallelHandler { + } + Ok(Err(e)) => { + results.push(BranchResult { +- id: String::new(), +- outcome: e.to_fail_outcome(), +- head_sha: None, ++ id: String::new(), ++ outcome: e.to_fail_outcome(), ++ head_sha: None, + worktree_path: None, + }); + } + Err(join_err) => { + results.push(BranchResult { +- id: String::new(), +- outcome: Outcome::fail_classify(format!( +- "task join error: {join_err}" +- )), +- head_sha: None, ++ id: String::new(), ++ outcome: Outcome::fail_classify(format!("task join error: {join_err}")), ++ head_sha: None, + worktree_path: None, + }); + } +diff --git a/lib/crates/fabro-workflow/src/handler/prompt.rs b/lib/crates/fabro-workflow/src/handler/prompt.rs +index df5e1f1c..b62c5022 100644 +--- a/lib/crates/fabro-workflow/src/handler/prompt.rs ++++ b/lib/crates/fabro-workflow/src/handler/prompt.rs +@@ -91,12 +91,12 @@ impl Handler for PromptHandler { + let stage_scope = StageScope::for_handler(context, &node.id); + services.emitter.emit_scoped( + &Event::Prompt { +- stage: node.id.clone(), +- visit: stage_scope.visit, +- text: prompt.clone(), +- mode: Some("prompt".to_string()), ++ stage: node.id.clone(), ++ visit: stage_scope.visit, ++ text: prompt.clone(), ++ mode: Some("prompt".to_string()), + provider: prompt_provider.clone(), +- model: prompt_model.clone(), ++ model: prompt_model.clone(), + }, + &stage_scope, + ); +@@ -143,11 +143,11 @@ impl Handler for PromptHandler { + + services.emitter.emit_scoped( + &Event::PromptCompleted { +- node_id: node.id.clone(), ++ node_id: node.id.clone(), + response: response_text.clone(), +- model: response_model, ++ model: response_model, + provider: response_provider, +- billing: stage_usage.clone(), ++ billing: stage_usage.clone(), + }, + &stage_scope, + ); +@@ -281,9 +281,9 @@ mod tests { + _system_prompt: Option<&str>, + ) -> Result { + Ok(CodergenResult::Text { +- text: "one-shot response".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "one-shot response".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -341,9 +341,9 @@ mod tests { + _system_prompt: Option<&str>, + ) -> Result { + Ok(CodergenResult::Text { +- text: "one-shot response".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "one-shot response".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -372,7 +372,7 @@ mod tests { + } + + struct OneShotCapturingBackend { +- captured_prompt: Arc>>, ++ captured_prompt: Arc>>, + captured_system_prompt: Arc>>>, + } + +@@ -400,9 +400,9 @@ mod tests { + *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); + *self.captured_system_prompt.lock().unwrap() = Some(system_prompt.map(String::from)); + Ok(CodergenResult::Text { +- text: "classified".to_string(), +- usage: None, +- files_touched: Vec::new(), ++ text: "classified".to_string(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -414,7 +414,7 @@ mod tests { + + let captured = Arc::new(Mutex::new(None)); + let backend = OneShotCapturingBackend { +- captured_prompt: captured.clone(), ++ captured_prompt: captured.clone(), + captured_system_prompt: Arc::new(Mutex::new(None)), + }; + let handler = PromptHandler::new(Some(Box::new(backend))); +@@ -451,7 +451,7 @@ mod tests { + + let captured_sys = Arc::new(Mutex::new(None)); + let backend = OneShotCapturingBackend { +- captured_prompt: Arc::new(Mutex::new(None)), ++ captured_prompt: Arc::new(Mutex::new(None)), + captured_system_prompt: captured_sys.clone(), + }; + let handler = PromptHandler::new(Some(Box::new(backend))); +@@ -484,7 +484,7 @@ mod tests { + + let captured_sys = Arc::new(Mutex::new(None)); + let backend = OneShotCapturingBackend { +- captured_prompt: Arc::new(Mutex::new(None)), ++ captured_prompt: Arc::new(Mutex::new(None)), + captured_system_prompt: captured_sys.clone(), + }; + let handler = PromptHandler::new(Some(Box::new(backend))); +diff --git a/lib/crates/fabro-workflow/src/lib.rs b/lib/crates/fabro-workflow/src/lib.rs +index d709327d..9eda2b32 100644 +--- a/lib/crates/fabro-workflow/src/lib.rs ++++ b/lib/crates/fabro-workflow/src/lib.rs +@@ -72,15 +72,15 @@ pub fn build_completed_stages(cp: &records::Checkpoint, run_failed: bool) -> Vec + last.failed = true; + } else { + stages.push(CompletedStage { +- node_id: "unknown".to_string(), +- status: "fail".to_string(), +- succeeded: false, +- failed: true, +- retries: 0, ++ node_id: "unknown".to_string(), ++ status: "fail".to_string(), ++ succeeded: false, ++ failed: true, ++ retries: 0, + billing_usd_micros: None, +- notes: None, +- failure_reason: None, +- files_touched: vec![], ++ notes: None, ++ failure_reason: None, ++ files_touched: vec![], + }); + } + } +diff --git a/lib/crates/fabro-workflow/src/lifecycle/artifact.rs b/lib/crates/fabro-workflow/src/lifecycle/artifact.rs +index 5cab268e..17a2e460 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/artifact.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/artifact.rs +@@ -34,15 +34,15 @@ const ARTIFACT_UPLOAD_RETRY_DELAYS: [Duration; 3] = [ + + /// Sub-lifecycle responsible for artifact collection, offloading, and syncing. + pub(crate) struct ArtifactLifecycle { +- pub sandbox: Arc, +- pub run_store: RunStoreHandle, +- pub emitter: Arc, +- pub run_id: RunId, +- pub artifact_globs: Vec, +- pub artifact_sink: Option, ++ pub sandbox: Arc, ++ pub run_store: RunStoreHandle, ++ pub emitter: Arc, ++ pub run_id: RunId, ++ pub artifact_globs: Vec, ++ pub artifact_sink: Option, + pub captured_artifact_count: Arc, + /// Per-attempt state: epoch seconds when the attempt started. +- attempt_start_epoch: std::sync::Mutex>, ++ attempt_start_epoch: std::sync::Mutex>, + } + + impl ArtifactLifecycle { +@@ -129,8 +129,8 @@ impl RunLifecycle for ArtifactLifecycle { + .await + { + self.emitter.emit(&Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "artifact_upload_failed".to_string(), ++ level: RunNoticeLevel::Warn, ++ code: "artifact_upload_failed".to_string(), + message: format!("[node: {node_id}] artifact upload failed: {err}"), + }); + return Ok(()); +@@ -140,14 +140,14 @@ impl RunLifecycle for ArtifactLifecycle { + self.captured_artifact_count.fetch_add(1, Ordering::Relaxed); + self.emitter.emit_scoped( + &Event::ArtifactCaptured { +- node_id: node_id.to_string(), +- attempt: ctx.attempt, +- node_slug: node_slug.clone(), +- path: asset.path.clone(), +- mime: asset.mime.clone(), +- content_md5: asset.content_md5.clone(), ++ node_id: node_id.to_string(), ++ attempt: ctx.attempt, ++ node_slug: node_slug.clone(), ++ path: asset.path.clone(), ++ mime: asset.mime.clone(), ++ content_md5: asset.content_md5.clone(), + content_sha256: asset.content_sha256.clone(), +- bytes: asset.bytes, ++ bytes: asset.bytes, + }, + &scope, + ); +@@ -156,8 +156,8 @@ impl RunLifecycle for ArtifactLifecycle { + Ok(_) => {} // no files collected + Err(e) => { + self.emitter.emit(&Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "artifact_collection_failed".to_string(), ++ level: RunNoticeLevel::Warn, ++ code: "artifact_collection_failed".to_string(), + message: format!("[node: {node_id}] artifact collection failed: {e}"), + }); + } +@@ -179,8 +179,8 @@ impl RunLifecycle for ArtifactLifecycle { + offload_large_values(&mut result.outcome.context_updates, &self.run_store).await + { + self.emitter.emit(&Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "artifact_offload_failed".to_string(), ++ level: RunNoticeLevel::Warn, ++ code: "artifact_offload_failed".to_string(), + message: format!("[node: {node_id}] artifact offload failed: {e}"), + }); + } +@@ -192,8 +192,8 @@ impl RunLifecycle for ArtifactLifecycle { + sync_artifacts_to_env(&mut result.outcome.context_updates, &*self.sandbox).await + { + self.emitter.emit(&Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "artifact_sync_failed".to_string(), ++ level: RunNoticeLevel::Warn, ++ code: "artifact_sync_failed".to_string(), + message: format!("[node: {node_id}] artifact sync failed: {e}"), + }); + } +diff --git a/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs b/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs +index 7b4bdf13..d2b6507a 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/circuit_breaker.rs +@@ -17,8 +17,8 @@ type WfNodeResult = NodeResult>; + /// Sub-lifecycle responsible for tracking failure signatures and tripping the + /// circuit breaker when deterministic failure cycles are detected. + pub(crate) struct CircuitBreakerLifecycle { +- loop_failure_signatures: Mutex>, +- restart_failure_signatures: Mutex>, ++ loop_failure_signatures: Mutex>, ++ restart_failure_signatures: Mutex>, + loop_restart_signature_limit: usize, + } + +diff --git a/lib/crates/fabro-workflow/src/lifecycle/event.rs b/lib/crates/fabro-workflow/src/lifecycle/event.rs +index 273e6d73..df5fafbc 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/event.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/event.rs +@@ -31,25 +31,25 @@ type FailureSignatureSnapshot = ( + + /// Sub-lifecycle responsible for emitting workflow run events. + pub(crate) struct EventLifecycle { +- pub emitter: Arc, +- pub graph_name: String, +- pub run_id: RunId, +- pub run_start: Mutex, ++ pub emitter: Arc, ++ pub graph_name: String, ++ pub run_id: RunId, ++ pub run_start: Mutex, + /// Set in on_edge_selected when loop_restart approved; emitted+cleared in + /// on_run_start. +- pub restarted_from: Arc>>, ++ pub restarted_from: Arc>>, + // Config for WorkflowRunStarted payload +- pub base_branch: Option, +- pub base_sha: Option, +- pub run_branch: Option, +- pub worktree_dir: Option, +- pub goal: Option, ++ pub base_branch: Option, ++ pub base_sha: Option, ++ pub run_branch: Option, ++ pub worktree_dir: Option, ++ pub goal: Option, + pub captured_artifact_count: Arc, + // Cross-lifecycle data +- pub checkpoint_git_result: Arc>>, +- pub last_git_sha: Arc>>, +- pub final_patch: Arc>>, +- pub circuit_breaker: Arc, ++ pub checkpoint_git_result: Arc>>, ++ pub last_git_sha: Arc>>, ++ pub final_patch: Arc>>, ++ pub circuit_breaker: Arc, + } + + fn snapshot_failure_signatures( +@@ -85,9 +85,9 @@ fn stage_visit(state: &WfRunState, node_id: &str) -> u32 { + + pub(crate) fn stage_scope_for(state: &WfRunState, node_id: &str) -> StageScope { + StageScope { +- node_id: node_id.to_string(), +- visit: stage_visit(state, node_id), +- parallel_group_id: state.context.parallel_group_id(), ++ node_id: node_id.to_string(), ++ visit: stage_visit(state, node_id), ++ parallel_group_id: state.context.parallel_group_id(), + parallel_branch_id: state.context.parallel_branch_id(), + } + } +@@ -109,13 +109,13 @@ impl RunLifecycle for EventLifecycle { + + // Emit RunStarted + self.emitter.emit(&Event::WorkflowRunStarted { +- name: self.graph_name.clone(), +- run_id: self.run_id, +- base_branch: self.base_branch.clone(), +- base_sha: self.base_sha.clone(), +- run_branch: self.run_branch.clone(), ++ name: self.graph_name.clone(), ++ run_id: self.run_id, ++ base_branch: self.base_branch.clone(), ++ base_sha: self.base_sha.clone(), ++ run_branch: self.run_branch.clone(), + worktree_dir: self.worktree_dir.clone(), +- goal: self.goal.clone(), ++ goal: self.goal.clone(), + }); + self.emitter.emit(&Event::RunRunning { reason: None }); + +@@ -138,11 +138,11 @@ impl RunLifecycle for EventLifecycle { + snapshot_failure_signatures(&self.circuit_breaker); + self.emitter.emit_scoped( + &Event::StageStarted { +- node_id: gv.id.clone(), +- name: gv.label().to_string(), +- index: stage_index, ++ node_id: gv.id.clone(), ++ name: gv.label().to_string(), ++ index: stage_index, + handler_type: gv.handler_type().unwrap_or_default().to_string(), +- attempt: 1, ++ attempt: 1, + max_attempts: 1, + }, + &scope, +@@ -186,11 +186,11 @@ impl RunLifecycle for EventLifecycle { + let scope = stage_scope_for(state, &gv.id); + self.emitter.emit_scoped( + &Event::StageStarted { +- node_id: gv.id.clone(), +- name: gv.label().to_string(), +- index: state.stage_index, ++ node_id: gv.id.clone(), ++ name: gv.label().to_string(), ++ index: state.stage_index, + handler_type: gv.handler_type().unwrap_or_default().to_string(), +- attempt: ctx.attempt as usize, ++ attempt: ctx.attempt as usize, + max_attempts: ctx.max_attempts as usize, + }, + &scope, +@@ -226,12 +226,12 @@ impl RunLifecycle for EventLifecycle { + + self.emitter.emit_scoped( + &Event::StageRetrying { +- node_id: gv.id.clone(), +- name: gv.label().to_string(), +- index: stage_index, +- attempt: ctx.attempt as usize, ++ node_id: gv.id.clone(), ++ name: gv.label().to_string(), ++ index: stage_index, ++ attempt: ctx.attempt as usize, + max_attempts: ctx.result.max_attempts as usize, +- delay_ms: ctx ++ delay_ms: ctx + .backoff_delay + .map_or(0, |d| u64::try_from(d.as_millis()).unwrap()), + }, +@@ -402,14 +402,14 @@ impl RunLifecycle for EventLifecycle { + self.emitter.emit_scoped( + &Event::GitCommit { + node_id: Some(node.id().to_string()), +- sha: sha.clone(), ++ sha: sha.clone(), + }, + &scope, + ); + } + for (branch, success) in &result.push_results { + self.emitter.emit(&Event::GitPush { +- branch: branch.clone(), ++ branch: branch.clone(), + success: *success, + }); + } +diff --git a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs +index c39eded7..019c8fde 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/fidelity.rs +@@ -29,11 +29,11 @@ struct IncomingEdgeData { + /// Sub-lifecycle responsible for fidelity/thread resolution and context key + /// setup. + pub(crate) struct FidelityLifecycle { +- pub graph: Arc, +- pub sandbox: Arc, +- pub run_store: RunStoreHandle, +- pub run_dir: PathBuf, +- incoming_edge_data: Mutex>, ++ pub graph: Arc, ++ pub sandbox: Arc, ++ pub run_store: RunStoreHandle, ++ pub run_dir: PathBuf, ++ incoming_edge_data: Mutex>, + /// True on the first node after checkpoint resume when prior fidelity was + /// Full. + degrade_fidelity_on_resume: Mutex, +diff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs +index faa6f10b..ca12e5a5 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/git.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs +@@ -54,24 +54,24 @@ fn build_checkpoint( + /// Result of a git checkpoint operation, shared with EventLifecycle. + #[derive(Debug, Clone)] + pub(crate) struct GitCheckpointResult { +- pub commit_sha: Option, ++ pub commit_sha: Option, + pub push_results: Vec<(String, bool)>, +- pub diff: Option, ++ pub diff: Option, + } + + /// Sub-lifecycle responsible for git operations (checkpoint commits, pushes, + /// diffs). + pub(crate) struct GitLifecycle { +- pub sandbox: Arc, +- pub emitter: Arc, +- pub run_id: RunId, +- pub run_store: RunStoreHandle, +- pub run_options: Arc, +- pub start_node_id: Option, ++ pub sandbox: Arc, ++ pub emitter: Arc, ++ pub run_id: RunId, ++ pub run_store: RunStoreHandle, ++ pub run_options: Arc, ++ pub start_node_id: Option, + // Cross-lifecycle data (shared with EventLifecycle) + pub checkpoint_git_result: Arc>>, +- pub last_git_sha: Arc>>, +- pub final_patch: Arc>>, ++ pub last_git_sha: Arc>>, ++ pub final_patch: Arc>>, + } + + #[async_trait] +@@ -165,8 +165,8 @@ impl RunLifecycle for GitLifecycle { + Ok(sha) => Some(sha), + Err(e) => { + self.emitter.emit(&Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "checkpoint_metadata_write_failed".to_string(), ++ level: RunNoticeLevel::Warn, ++ code: "checkpoint_metadata_write_failed".to_string(), + message: format!( + "[node: {node_id}] metadata checkpoint write failed: {e}" + ), +@@ -199,9 +199,9 @@ impl RunLifecycle for GitLifecycle { + match commit_result { + Ok(sha) => { + let mut git_result = GitCheckpointResult { +- commit_sha: Some(sha.clone()), ++ commit_sha: Some(sha.clone()), + push_results: Vec::new(), +- diff: None, ++ diff: None, + }; + + // Push run branch (skip in dry-run mode) +@@ -270,8 +270,8 @@ impl RunLifecycle for GitLifecycle { + Ok(_) => {} + Err(err) => { + self.emitter.emit(&Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "git_diff_failed".to_string(), ++ level: RunNoticeLevel::Warn, ++ code: "git_diff_failed".to_string(), + message: format!("[node: {node_id}] git diff failed: {err}"), + }); + } +@@ -287,7 +287,7 @@ impl RunLifecycle for GitLifecycle { + self.emitter.emit_scoped( + &Event::CheckpointFailed { + node_id: node_id.to_string(), +- error: e.clone(), ++ error: e.clone(), + }, + &scope, + ); +@@ -321,8 +321,8 @@ impl RunLifecycle for GitLifecycle { + Err(err) => { + *self.final_patch.lock().unwrap() = None; + self.emitter.emit(&Event::RunNotice { +- level: RunNoticeLevel::Warn, +- code: "git_diff_failed".to_string(), ++ level: RunNoticeLevel::Warn, ++ code: "git_diff_failed".to_string(), + message: format!("final diff failed: {err}"), + }); + } +diff --git a/lib/crates/fabro-workflow/src/lifecycle/hook.rs b/lib/crates/fabro-workflow/src/lifecycle/hook.rs +index f3d66a67..90f35da4 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/hook.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/hook.rs +@@ -22,11 +22,11 @@ type WfNodeDecision = NodeDecision>; + + /// Sub-lifecycle responsible for running workflow hooks. + pub(crate) struct HookLifecycle { +- pub hook_runner: Option>, +- pub sandbox: Arc, ++ pub hook_runner: Option>, ++ pub sandbox: Arc, + pub hook_work_dir: Option, +- pub run_id: RunId, +- pub graph_name: String, ++ pub run_id: RunId, ++ pub graph_name: String, + } + + impl HookLifecycle { +diff --git a/lib/crates/fabro-workflow/src/lifecycle/mod.rs b/lib/crates/fabro-workflow/src/lifecycle/mod.rs +index a597391f..8db96493 100644 +--- a/lib/crates/fabro-workflow/src/lifecycle/mod.rs ++++ b/lib/crates/fabro-workflow/src/lifecycle/mod.rs +@@ -49,28 +49,28 @@ type WfNodeDecision = NodeDecision>; + /// Orchestrates all sub-lifecycles with explicit per-callback ordering. + /// Implements `RunLifecycle` by delegating to focused structs. + pub(crate) struct WorkflowLifecycle { +- event: EventLifecycle, +- hook: HookLifecycle, +- fidelity: FidelityLifecycle, +- auto_status: AutoStatusLifecycle, +- circuit_breaker: Arc, +- git: GitLifecycle, +- artifact: ArtifactLifecycle, +- on_node: crate::OnNodeCallback, +- emitter: Arc, +- run_control: Option>, ++ event: EventLifecycle, ++ hook: HookLifecycle, ++ fidelity: FidelityLifecycle, ++ auto_status: AutoStatusLifecycle, ++ circuit_breaker: Arc, ++ git: GitLifecycle, ++ artifact: ArtifactLifecycle, ++ on_node: crate::OnNodeCallback, ++ emitter: Arc, ++ run_control: Option>, + /// Set in on_edge_selected when loop_restart approved; read+cleared by + /// EventLifecycle::on_run_start +- restarted_from: Arc>>, ++ restarted_from: Arc>>, + /// Shared git checkpoint result (written by git, read by event) + checkpoint_git_result: Arc>>, + /// True when constructed with a checkpoint; cleared after first + /// on_run_start. Gates context seeding on initial resume. +- is_initial_resume: AtomicBool, ++ is_initial_resume: AtomicBool, + // Config needed for context seeding +- graph: Arc, +- run_id: RunId, +- working_directory: Option, ++ graph: Arc, ++ run_id: RunId, ++ working_directory: Option, + } + + impl WorkflowLifecycle { +@@ -111,21 +111,21 @@ impl WorkflowLifecycle { + }; + + let event = EventLifecycle { +- emitter: Arc::clone(emitter), +- graph_name: graph.name.clone(), +- run_id: run_options.run_id, +- run_start: Mutex::new(Instant::now()), +- restarted_from: Arc::clone(&restarted_from), +- base_branch: run_options.base_branch.clone(), +- base_sha: run_options.git.as_ref().and_then(|g| g.base_sha.clone()), +- run_branch: run_options.git.as_ref().and_then(|g| g.run_branch.clone()), +- worktree_dir: working_directory.clone(), +- goal: (!graph.goal().is_empty()).then(|| graph.goal().to_string()), ++ emitter: Arc::clone(emitter), ++ graph_name: graph.name.clone(), ++ run_id: run_options.run_id, ++ run_start: Mutex::new(Instant::now()), ++ restarted_from: Arc::clone(&restarted_from), ++ base_branch: run_options.base_branch.clone(), ++ base_sha: run_options.git.as_ref().and_then(|g| g.base_sha.clone()), ++ run_branch: run_options.git.as_ref().and_then(|g| g.run_branch.clone()), ++ worktree_dir: working_directory.clone(), ++ goal: (!graph.goal().is_empty()).then(|| graph.goal().to_string()), + captured_artifact_count: Arc::clone(&captured_artifact_count), +- last_git_sha: Arc::clone(&last_git_sha), +- final_patch: Arc::clone(&final_patch), +- checkpoint_git_result: Arc::clone(&checkpoint_git_result), +- circuit_breaker: Arc::clone(&circuit_breaker), ++ last_git_sha: Arc::clone(&last_git_sha), ++ final_patch: Arc::clone(&final_patch), ++ checkpoint_git_result: Arc::clone(&checkpoint_git_result), ++ circuit_breaker: Arc::clone(&circuit_breaker), + }; + + let hook = HookLifecycle { +diff --git a/lib/crates/fabro-workflow/src/node_handler.rs b/lib/crates/fabro-workflow/src/node_handler.rs +index 98018ffd..80f12515 100644 +--- a/lib/crates/fabro-workflow/src/node_handler.rs ++++ b/lib/crates/fabro-workflow/src/node_handler.rs +@@ -26,8 +26,8 @@ use crate::retry::build_retry_policy; + /// then diffs and applies changes back. + pub(crate) struct WorkflowNodeHandler { + pub services: Arc, +- pub run_dir: PathBuf, +- pub graph: Arc, ++ pub run_dir: PathBuf, ++ pub graph: Arc, + } + + #[async_trait] +@@ -50,9 +50,9 @@ impl NodeHandler for WorkflowNodeHandler { + .await + .map_err(|err| { + CoreError::handler(HandlerErrorDetail { +- message: err.to_string(), ++ message: err.to_string(), + retryable: true, +- category: Some(FailureCategory::TransientInfra), ++ category: Some(FailureCategory::TransientInfra), + signature: None, + }) + })?; +@@ -78,9 +78,9 @@ impl NodeHandler for WorkflowNodeHandler { + Ok(inner) => inner, + Err(_elapsed) => { + return Err(CoreError::handler(HandlerErrorDetail { +- message: format!("handler timed out after {}ms", duration.as_millis()), ++ message: format!("handler timed out after {}ms", duration.as_millis()), + retryable: true, +- category: Some(FailureCategory::TransientInfra), ++ category: Some(FailureCategory::TransientInfra), + signature: None, + })); + } +@@ -114,9 +114,9 @@ impl NodeHandler for WorkflowNodeHandler { + Err(panic_payload) => { + let msg = format_panic_message(&panic_payload); + Err(CoreError::handler(HandlerErrorDetail { +- message: msg, ++ message: msg, + retryable: false, +- category: Some(FailureCategory::Deterministic), ++ category: Some(FailureCategory::Deterministic), + signature: None, + })) + } +diff --git a/lib/crates/fabro-workflow/src/operations/create.rs b/lib/crates/fabro-workflow/src/operations/create.rs +index 6fbda634..501c6ea6 100644 +--- a/lib/crates/fabro-workflow/src/operations/create.rs ++++ b/lib/crates/fabro-workflow/src/operations/create.rs +@@ -46,22 +46,22 @@ pub struct CreateRunInput { + #[derive(Debug)] + pub struct CreatedRun { + pub persisted: Persisted, +- pub run_id: RunId, +- pub run_dir: PathBuf, +- pub dot_path: Option, ++ pub run_id: RunId, ++ pub run_dir: PathBuf, ++ pub dot_path: Option, + } + + struct PersistCreateOptions { +- settings: SettingsLayer, +- run_id: Option, +- run_dir: Option, +- workflow_slug: Option, +- labels: HashMap, +- base_branch: Option, +- working_directory: PathBuf, +- host_repo_path: Option, +- repo_origin_url: Option, +- provenance: Option, ++ settings: SettingsLayer, ++ run_id: Option, ++ run_dir: Option, ++ workflow_slug: Option, ++ labels: HashMap, ++ base_branch: Option, ++ working_directory: PathBuf, ++ host_repo_path: Option, ++ repo_origin_url: Option, ++ provenance: Option, + configured_providers: Vec, + } + +@@ -70,7 +70,7 @@ pub async fn create(store: &Database, request: CreateRunInput) -> Result PathBuf { +@@ -455,7 +465,7 @@ mod tests { + fn validate_dot(dot_source: &str, settings: SettingsLayer) -> Validated { + validate(ValidateInput { + workflow: WorkflowInput::DotSource { +- source: dot_source.to_string(), ++ source: dot_source.to_string(), + base_dir: None, + }, + settings, +@@ -574,12 +584,12 @@ mod tests { + #[test] + fn validate_returns_error_on_invalid_dot() { + let result = validate(ValidateInput { +- workflow: WorkflowInput::DotSource { +- source: "not a graph".to_string(), ++ workflow: WorkflowInput::DotSource { ++ source: "not a graph".to_string(), + base_dir: None, + }, +- settings: SettingsLayer::default(), +- cwd: PathBuf::from("."), ++ settings: SettingsLayer::default(), ++ cwd: PathBuf::from("."), + custom_transforms: Vec::new(), + }); + assert!(result.is_err()); +@@ -617,12 +627,12 @@ mod tests { + } + + let validated = validate(ValidateInput { +- workflow: WorkflowInput::DotSource { +- source: MINIMAL_DOT.to_string(), ++ workflow: WorkflowInput::DotSource { ++ source: MINIMAL_DOT.to_string(), + base_dir: None, + }, +- settings: SettingsLayer::default(), +- cwd: PathBuf::from("."), ++ settings: SettingsLayer::default(), ++ cwd: PathBuf::from("."), + custom_transforms: vec![Box::new(TagTransform)], + }) + .unwrap(); +@@ -653,9 +663,9 @@ mod tests { + .unwrap(); + + let validated = validate(ValidateInput { +- workflow: WorkflowInput::Path(dot_path), +- settings: SettingsLayer::default(), +- cwd: dir.path().to_path_buf(), ++ workflow: WorkflowInput::Path(dot_path), ++ settings: SettingsLayer::default(), ++ cwd: dir.path().to_path_buf(), + custom_transforms: Vec::new(), + }) + .unwrap(); +@@ -666,9 +676,9 @@ mod tests { + #[test] + fn validate_from_bundle_resolves_nested_import_files_relative_to_imported_graph() { + let validated = validate(ValidateInput { +- workflow: WorkflowInput::Bundled(BundledWorkflow { ++ workflow: WorkflowInput::Bundled(BundledWorkflow { + logical_path: PathBuf::from("workflow.fabro"), +- source: r#"digraph Test { ++ source: r#"digraph Test { + graph [goal="Ship"] + start [shape=Mdiamond] + validate [import="./child/validate.fabro"] +@@ -676,7 +686,7 @@ mod tests { + start -> validate -> exit + }"# + .to_string(), +- files: HashMap::from([ ++ files: HashMap::from([ + ( + PathBuf::from("child/validate.fabro"), + r#"digraph Validate { +@@ -693,8 +703,8 @@ mod tests { + ), + ]), + }), +- settings: SettingsLayer::default(), +- cwd: PathBuf::from("."), ++ settings: SettingsLayer::default(), ++ cwd: PathBuf::from("."), + custom_transforms: Vec::new(), + }) + .unwrap(); +@@ -717,24 +727,27 @@ mod tests { + }"#; + let dir = tempfile::tempdir().unwrap(); + let store = memory_store(); +- let err = create(&store, CreateRunInput { +- workflow: WorkflowInput::DotSource { +- source: dot.to_string(), +- base_dir: None, ++ let err = create( ++ &store, ++ CreateRunInput { ++ workflow: WorkflowInput::DotSource { ++ source: dot.to_string(), ++ base_dir: None, ++ }, ++ settings: SettingsLayer::default(), ++ cwd: dir.path().to_path_buf(), ++ workflow_slug: None, ++ workflow_path: None, ++ workflow_bundle: None, ++ submitted_manifest_bytes: None, ++ run_id: None, ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ provenance: None, ++ configured_providers: Vec::new(), + }, +- settings: SettingsLayer::default(), +- cwd: dir.path().to_path_buf(), +- workflow_slug: None, +- workflow_path: None, +- workflow_bundle: None, +- submitted_manifest_bytes: None, +- run_id: None, +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- provenance: None, +- configured_providers: Vec::new(), +- }) ++ ) + .await + .unwrap_err(); + +@@ -750,51 +763,54 @@ mod tests { + async fn create_persists_normalized_config_and_initial_state() { + let dir = tempfile::tempdir().unwrap(); + let store = memory_store(); +- let created = create(&store, CreateRunInput { +- workflow: WorkflowInput::DotSource { +- source: MINIMAL_DOT.to_string(), +- base_dir: None, +- }, +- settings: { +- use fabro_types::settings::run::{ +- RunExecutionLayer, RunGoalLayer, RunLayer, RunMode, RunModelLayer, +- RunPullRequestLayer, +- }; +- let mut metadata = HashMap::new(); +- metadata.insert("env".to_string(), "test".to_string()); +- SettingsLayer { +- run: Some(RunLayer { +- goal: Some(RunGoalLayer::Inline(InterpString::parse("override goal"))), +- metadata, +- model: Some(RunModelLayer { +- name: Some(InterpString::parse("sonnet")), +- ..RunModelLayer::default() ++ let created = create( ++ &store, ++ CreateRunInput { ++ workflow: WorkflowInput::DotSource { ++ source: MINIMAL_DOT.to_string(), ++ base_dir: None, ++ }, ++ settings: { ++ use fabro_types::settings::run::{ ++ RunExecutionLayer, RunGoalLayer, RunLayer, RunMode, RunModelLayer, ++ RunPullRequestLayer, ++ }; ++ let mut metadata = HashMap::new(); ++ metadata.insert("env".to_string(), "test".to_string()); ++ SettingsLayer { ++ run: Some(RunLayer { ++ goal: Some(RunGoalLayer::Inline(InterpString::parse("override goal"))), ++ metadata, ++ model: Some(RunModelLayer { ++ name: Some(InterpString::parse("sonnet")), ++ ..RunModelLayer::default() ++ }), ++ pull_request: Some(RunPullRequestLayer { ++ enabled: Some(false), ++ ..RunPullRequestLayer::default() ++ }), ++ execution: Some(RunExecutionLayer { ++ mode: Some(RunMode::DryRun), ++ ..RunExecutionLayer::default() ++ }), ++ ..RunLayer::default() + }), +- pull_request: Some(RunPullRequestLayer { +- enabled: Some(false), +- ..RunPullRequestLayer::default() +- }), +- execution: Some(RunExecutionLayer { +- mode: Some(RunMode::DryRun), +- ..RunExecutionLayer::default() +- }), +- ..RunLayer::default() +- }), +- ..SettingsLayer::default() +- } ++ ..SettingsLayer::default() ++ } ++ }, ++ cwd: dir.path().to_path_buf(), ++ workflow_slug: Some("slug".to_string()), ++ workflow_path: None, ++ workflow_bundle: None, ++ submitted_manifest_bytes: None, ++ run_id: Some(fixtures::RUN_1), ++ host_repo_path: Some(dir.path().display().to_string()), ++ repo_origin_url: None, ++ base_branch: Some("main".to_string()), ++ provenance: None, ++ configured_providers: Vec::new(), + }, +- cwd: dir.path().to_path_buf(), +- workflow_slug: Some("slug".to_string()), +- workflow_path: None, +- workflow_bundle: None, +- submitted_manifest_bytes: None, +- run_id: Some(fixtures::RUN_1), +- host_repo_path: Some(dir.path().display().to_string()), +- repo_origin_url: None, +- base_branch: Some("main".to_string()), +- provenance: None, +- configured_providers: Vec::new(), +- }) ++ ) + .await + .unwrap(); + +@@ -859,37 +875,40 @@ mod tests { + std::fs::create_dir_all(&workspace).unwrap(); + + let store = memory_store(); +- let created = create(&store, CreateRunInput { +- workflow: WorkflowInput::DotSource { +- source: MINIMAL_DOT.to_string(), +- base_dir: None, +- }, +- settings: { +- use fabro_types::settings::run::{RunExecutionLayer, RunLayer, RunMode}; +- SettingsLayer { +- run: Some(RunLayer { +- working_dir: Some(InterpString::parse("workspace")), +- execution: Some(RunExecutionLayer { +- mode: Some(RunMode::DryRun), +- ..RunExecutionLayer::default() ++ let created = create( ++ &store, ++ CreateRunInput { ++ workflow: WorkflowInput::DotSource { ++ source: MINIMAL_DOT.to_string(), ++ base_dir: None, ++ }, ++ settings: { ++ use fabro_types::settings::run::{RunExecutionLayer, RunLayer, RunMode}; ++ SettingsLayer { ++ run: Some(RunLayer { ++ working_dir: Some(InterpString::parse("workspace")), ++ execution: Some(RunExecutionLayer { ++ mode: Some(RunMode::DryRun), ++ ..RunExecutionLayer::default() ++ }), ++ ..RunLayer::default() + }), +- ..RunLayer::default() +- }), +- ..SettingsLayer::default() +- } ++ ..SettingsLayer::default() ++ } ++ }, ++ cwd: dir.path().to_path_buf(), ++ workflow_slug: None, ++ workflow_path: None, ++ workflow_bundle: None, ++ submitted_manifest_bytes: None, ++ run_id: Some(fixtures::RUN_2), ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ provenance: None, ++ configured_providers: Vec::new(), + }, +- cwd: dir.path().to_path_buf(), +- workflow_slug: None, +- workflow_path: None, +- workflow_bundle: None, +- submitted_manifest_bytes: None, +- run_id: Some(fixtures::RUN_2), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- provenance: None, +- configured_providers: Vec::new(), +- }) ++ ) + .await + .unwrap(); + +@@ -911,24 +930,27 @@ mod tests { + async fn create_persists_repo_origin_url_from_request() { + let dir = tempfile::tempdir().unwrap(); + let store = memory_store(); +- let created = create(&store, CreateRunInput { +- workflow: WorkflowInput::DotSource { +- source: MINIMAL_DOT.to_string(), +- base_dir: None, ++ let created = create( ++ &store, ++ CreateRunInput { ++ workflow: WorkflowInput::DotSource { ++ source: MINIMAL_DOT.to_string(), ++ base_dir: None, ++ }, ++ settings: dry_run_only_settings(), ++ cwd: dir.path().to_path_buf(), ++ workflow_slug: None, ++ workflow_path: None, ++ workflow_bundle: None, ++ submitted_manifest_bytes: None, ++ run_id: Some(fixtures::RUN_2), ++ host_repo_path: None, ++ repo_origin_url: Some("https://github.com/acme/widgets".to_string()), ++ base_branch: None, ++ provenance: None, ++ configured_providers: Vec::new(), + }, +- settings: dry_run_only_settings(), +- cwd: dir.path().to_path_buf(), +- workflow_slug: None, +- workflow_path: None, +- workflow_bundle: None, +- submitted_manifest_bytes: None, +- run_id: Some(fixtures::RUN_2), +- host_repo_path: None, +- repo_origin_url: Some("https://github.com/acme/widgets".to_string()), +- base_branch: None, +- provenance: None, +- configured_providers: Vec::new(), +- }) ++ ) + .await + .unwrap(); + +@@ -986,24 +1008,27 @@ mod tests { + Duration::from_millis(1), + None, + )); +- let created = create(store.as_ref(), CreateRunInput { +- workflow: WorkflowInput::DotSource { +- source: MINIMAL_DOT.to_string(), +- base_dir: None, ++ let created = create( ++ store.as_ref(), ++ CreateRunInput { ++ workflow: WorkflowInput::DotSource { ++ source: MINIMAL_DOT.to_string(), ++ base_dir: None, ++ }, ++ settings: dry_run_with_storage(&storage_dir), ++ cwd: dir.path().to_path_buf(), ++ workflow_slug: Some("slug".to_string()), ++ workflow_path: None, ++ workflow_bundle: None, ++ submitted_manifest_bytes: None, ++ run_id: Some(fixtures::RUN_3), ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ provenance: None, ++ configured_providers: Vec::new(), + }, +- settings: dry_run_with_storage(&storage_dir), +- cwd: dir.path().to_path_buf(), +- workflow_slug: Some("slug".to_string()), +- workflow_path: None, +- workflow_bundle: None, +- submitted_manifest_bytes: None, +- run_id: Some(fixtures::RUN_3), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- provenance: None, +- configured_providers: Vec::new(), +- }) ++ ) + .await + .unwrap(); + let run_store = store.open_run_reader(&created.run_id).await.unwrap(); +@@ -1028,37 +1053,40 @@ mod tests { + Duration::from_millis(1), + None, + )); +- let created = create(store.as_ref(), CreateRunInput { +- workflow: WorkflowInput::DotSource { +- source: MINIMAL_DOT.to_string(), +- base_dir: None, +- }, +- settings: dry_run_with_storage(&storage_dir), +- cwd: dir.path().to_path_buf(), +- workflow_slug: Some("slug".to_string()), +- workflow_path: None, +- workflow_bundle: None, +- submitted_manifest_bytes: None, +- run_id: Some(fixtures::RUN_64), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- provenance: Some(fabro_types::RunProvenance { +- server: Some(fabro_types::RunServerProvenance { +- version: "0.9.0".to_string(), +- }), +- client: Some(fabro_types::RunClientProvenance { +- user_agent: Some("fabro-cli/0.9.0".to_string()), +- name: Some("fabro-cli".to_string()), +- version: Some("0.9.0".to_string()), +- }), +- subject: Some(fabro_types::RunSubjectProvenance { +- login: None, +- auth_method: fabro_types::RunAuthMethod::Disabled, ++ let created = create( ++ store.as_ref(), ++ CreateRunInput { ++ workflow: WorkflowInput::DotSource { ++ source: MINIMAL_DOT.to_string(), ++ base_dir: None, ++ }, ++ settings: dry_run_with_storage(&storage_dir), ++ cwd: dir.path().to_path_buf(), ++ workflow_slug: Some("slug".to_string()), ++ workflow_path: None, ++ workflow_bundle: None, ++ submitted_manifest_bytes: None, ++ run_id: Some(fixtures::RUN_64), ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ provenance: Some(fabro_types::RunProvenance { ++ server: Some(fabro_types::RunServerProvenance { ++ version: "0.9.0".to_string(), ++ }), ++ client: Some(fabro_types::RunClientProvenance { ++ user_agent: Some("fabro-cli/0.9.0".to_string()), ++ name: Some("fabro-cli".to_string()), ++ version: Some("0.9.0".to_string()), ++ }), ++ subject: Some(fabro_types::RunSubjectProvenance { ++ login: None, ++ auth_method: fabro_types::RunAuthMethod::Disabled, ++ }), + }), +- }), +- configured_providers: Vec::new(), +- }) ++ configured_providers: Vec::new(), ++ }, ++ ) + .await + .unwrap(); + +diff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs +index 8f039fa0..07c8f624 100644 +--- a/lib/crates/fabro-workflow/src/operations/fork.rs ++++ b/lib/crates/fabro-workflow/src/operations/fork.rs +@@ -11,8 +11,8 @@ use crate::records::{Checkpoint, RunRecord, StartRecord}; + #[derive(Debug, Clone)] + pub struct ForkRunInput { + pub source_run_id: RunId, +- pub target: Option, +- pub push: bool, ++ pub target: Option, ++ pub push: bool, + } + + /// Create a new run that branches from an existing run at a specific +@@ -89,10 +89,10 @@ fn fork_from_entry( + + let now = new_run_id.created_at(); + let start_record = StartRecord { +- run_id: new_run_id, ++ run_id: new_run_id, + start_time: now, + run_branch: Some(new_run_branch.clone()), +- base_sha: None, ++ base_sha: None, + }; + let new_start_record_bytes = + serde_json::to_vec_pretty(&start_record).context("failed to serialize new start.json")?; +@@ -243,11 +243,14 @@ mod tests { + let source_run_id = parse_run_id("01ARZ3NDEKTSV4RRFFQ69G5FAV"); + let run_oids = setup_source_run(&store, &source_run_id, &["start", "build", "test"]); + +- let new_run_id = fork(&store, &ForkRunInput { +- source_run_id, +- target: Some(RewindTarget::from_str("@2").unwrap()), +- push: false, +- }) ++ let new_run_id = fork( ++ &store, ++ &ForkRunInput { ++ source_run_id, ++ target: Some(RewindTarget::from_str("@2").unwrap()), ++ push: false, ++ }, ++ ) + .unwrap(); + + let new_run_branch = format!("{RUN_BRANCH_PREFIX}{new_run_id}"); +@@ -287,11 +290,11 @@ mod tests { + .write_entry("checkpoint.json", &cp, "checkpoint") + .unwrap(); + let entry = TimelineEntry { +- ordinal: 1, +- node_name: "start".to_string(), +- visit: 1, ++ ordinal: 1, ++ node_name: "start".to_string(), ++ visit: 1, + metadata_commit_oid: oid, +- run_commit_sha: None, ++ run_commit_sha: None, + }; + + let err = fork_from_entry(&store, &run_id, &entry, false) +diff --git a/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs b/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs +index 923d5cd6..ab24a1ef 100644 +--- a/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs ++++ b/lib/crates/fabro-workflow/src/operations/rebuild_meta.rs +@@ -166,7 +166,7 @@ pub async fn build_timeline_or_rebuild( + } + + Ok(RunTimeline { +- entries: Vec::new(), ++ entries: Vec::new(), + parallel_map: HashMap::new(), + }) + } +@@ -398,11 +398,11 @@ mod tests { + + fn sample_sandbox_record() -> SandboxRecord { + SandboxRecord { +- provider: "local".to_string(), +- working_directory: "/tmp/project".to_string(), +- identifier: None, ++ provider: "local".to_string(), ++ working_directory: "/tmp/project".to_string(), ++ identifier: None, + host_working_directory: None, +- container_mount_point: None, ++ container_mount_point: None, + } + } + +@@ -413,20 +413,20 @@ mod tests { + git_commit_sha: Option<&str>, + ) -> Checkpoint { + Checkpoint { +- timestamp: created_at(), +- current_node: current_node.to_string(), +- completed_nodes: completed_nodes ++ timestamp: created_at(), ++ current_node: current_node.to_string(), ++ completed_nodes: completed_nodes + .iter() + .map(|node| (*node).to_string()) + .collect(), +- node_retries: HashMap::new(), +- context_values: HashMap::new(), +- node_outcomes: HashMap::new(), +- next_node_id: None, +- git_commit_sha: git_commit_sha.map(ToOwned::to_owned), +- loop_failure_signatures: HashMap::new(), ++ node_retries: HashMap::new(), ++ context_values: HashMap::new(), ++ node_outcomes: HashMap::new(), ++ next_node_id: None, ++ git_commit_sha: git_commit_sha.map(ToOwned::to_owned), ++ loop_failure_signatures: HashMap::new(), + restart_failure_signatures: HashMap::new(), +- node_visits: node_visits ++ node_visits: node_visits + .iter() + .map(|(node, visit)| ((*node).to_string(), *visit)) + .collect(), +@@ -440,23 +440,27 @@ mod tests { + ) -> DurableRunStore { + let run_store = store.create_run(&run_id).await.unwrap(); + let run_record = sample_run_record(run_id, host_repo_path); +- append_event(&run_store, &run_id, &Event::RunCreated { +- run_id, +- settings: serde_json::to_value(&run_record.settings).unwrap(), +- graph: serde_json::to_value(&run_record.graph).unwrap(), +- workflow_source: None, +- workflow_config: None, +- labels: run_record.labels.clone().into_iter().collect(), +- run_dir: String::new(), +- working_directory: run_record.working_directory.display().to_string(), +- host_repo_path: run_record.host_repo_path.clone(), +- repo_origin_url: run_record.repo_origin_url.clone(), +- base_branch: run_record.base_branch.clone(), +- workflow_slug: run_record.workflow_slug.clone(), +- db_prefix: None, +- provenance: run_record.provenance.clone(), +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &run_id, ++ &Event::RunCreated { ++ run_id, ++ settings: serde_json::to_value(&run_record.settings).unwrap(), ++ graph: serde_json::to_value(&run_record.graph).unwrap(), ++ workflow_source: None, ++ workflow_config: None, ++ labels: run_record.labels.clone().into_iter().collect(), ++ run_dir: String::new(), ++ working_directory: run_record.working_directory.display().to_string(), ++ host_repo_path: run_record.host_repo_path.clone(), ++ repo_origin_url: run_record.repo_origin_url.clone(), ++ base_branch: run_record.base_branch.clone(), ++ workflow_slug: run_record.workflow_slug.clone(), ++ db_prefix: None, ++ provenance: run_record.provenance.clone(), ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); + run_store +@@ -464,28 +468,36 @@ mod tests { + + async fn append_start_event(run_store: &DurableRunStore, run_id: RunId) { + let start = sample_start_record(run_id); +- append_event(run_store, &run_id, &Event::WorkflowRunStarted { +- name: "test".to_string(), +- run_id, +- base_branch: None, +- base_sha: start.base_sha, +- run_branch: start.run_branch, +- worktree_dir: None, +- goal: None, +- }) ++ append_event( ++ run_store, ++ &run_id, ++ &Event::WorkflowRunStarted { ++ name: "test".to_string(), ++ run_id, ++ base_branch: None, ++ base_sha: start.base_sha, ++ run_branch: start.run_branch, ++ worktree_dir: None, ++ goal: None, ++ }, ++ ) + .await + .unwrap(); + } + + async fn append_sandbox_event(run_store: &DurableRunStore, run_id: RunId) { + let sandbox = sample_sandbox_record(); +- append_event(run_store, &run_id, &Event::SandboxInitialized { +- provider: sandbox.provider, +- working_directory: sandbox.working_directory, +- identifier: sandbox.identifier, +- host_working_directory: sandbox.host_working_directory, +- container_mount_point: sandbox.container_mount_point, +- }) ++ append_event( ++ run_store, ++ &run_id, ++ &Event::SandboxInitialized { ++ provider: sandbox.provider, ++ working_directory: sandbox.working_directory, ++ identifier: sandbox.identifier, ++ host_working_directory: sandbox.host_working_directory, ++ container_mount_point: sandbox.container_mount_point, ++ }, ++ ) + .await + .unwrap(); + } +@@ -495,31 +507,35 @@ mod tests { + run_id: RunId, + checkpoint: Checkpoint, + ) { +- append_event(run_store, &run_id, &Event::CheckpointCompleted { +- node_id: checkpoint.current_node.clone(), +- status: "success".to_string(), +- current_node: checkpoint.current_node.clone(), +- completed_nodes: checkpoint.completed_nodes.clone(), +- node_retries: checkpoint.node_retries.clone().into_iter().collect(), +- context_values: checkpoint.context_values.clone().into_iter().collect(), +- node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), +- next_node_id: checkpoint.next_node_id.clone(), +- git_commit_sha: checkpoint.git_commit_sha.clone(), +- loop_failure_signatures: checkpoint +- .loop_failure_signatures +- .clone() +- .into_iter() +- .map(|(signature, count)| (signature.to_string(), count)) +- .collect(), +- restart_failure_signatures: checkpoint +- .restart_failure_signatures +- .clone() +- .into_iter() +- .map(|(signature, count)| (signature.to_string(), count)) +- .collect(), +- node_visits: checkpoint.node_visits.clone().into_iter().collect(), +- diff: None, +- }) ++ append_event( ++ run_store, ++ &run_id, ++ &Event::CheckpointCompleted { ++ node_id: checkpoint.current_node.clone(), ++ status: "success".to_string(), ++ current_node: checkpoint.current_node.clone(), ++ completed_nodes: checkpoint.completed_nodes.clone(), ++ node_retries: checkpoint.node_retries.clone().into_iter().collect(), ++ context_values: checkpoint.context_values.clone().into_iter().collect(), ++ node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), ++ next_node_id: checkpoint.next_node_id.clone(), ++ git_commit_sha: checkpoint.git_commit_sha.clone(), ++ loop_failure_signatures: checkpoint ++ .loop_failure_signatures ++ .clone() ++ .into_iter() ++ .map(|(signature, count)| (signature.to_string(), count)) ++ .collect(), ++ restart_failure_signatures: checkpoint ++ .restart_failure_signatures ++ .clone() ++ .into_iter() ++ .map(|(signature, count)| (signature.to_string(), count)) ++ .collect(), ++ node_visits: checkpoint.node_visits.clone().into_iter().collect(), ++ diff: None, ++ }, ++ ) + .await + .unwrap(); + } +@@ -530,14 +546,18 @@ mod tests { + node: &StageId, + text: &str, + ) { +- append_event(run_store, &run_id, &Event::Prompt { +- stage: node.node_id().to_string(), +- visit: node.visit(), +- text: text.to_string(), +- mode: None, +- provider: None, +- model: None, +- }) ++ append_event( ++ run_store, ++ &run_id, ++ &Event::Prompt { ++ stage: node.node_id().to_string(), ++ visit: node.visit(), ++ text: text.to_string(), ++ mode: None, ++ provider: None, ++ model: None, ++ }, ++ ) + .await + .unwrap(); + } +diff --git a/lib/crates/fabro-workflow/src/operations/rewind.rs b/lib/crates/fabro-workflow/src/operations/rewind.rs +index fef41e0f..8ec98435 100644 +--- a/lib/crates/fabro-workflow/src/operations/rewind.rs ++++ b/lib/crates/fabro-workflow/src/operations/rewind.rs +@@ -51,16 +51,16 @@ impl FromStr for RewindTarget { + + #[derive(Debug, Clone)] + pub struct TimelineEntry { +- pub ordinal: usize, +- pub node_name: String, +- pub visit: usize, ++ pub ordinal: usize, ++ pub node_name: String, ++ pub visit: usize, + pub metadata_commit_oid: Oid, +- pub run_commit_sha: Option, ++ pub run_commit_sha: Option, + } + + #[derive(Debug, Clone)] + pub struct RunTimeline { +- pub entries: Vec, ++ pub entries: Vec, + pub parallel_map: HashMap, + } + +@@ -116,7 +116,7 @@ impl RunTimeline { + pub struct RewindInput { + pub run_id: RunId, + pub target: RewindTarget, +- pub push: bool, ++ pub push: bool, + } + + pub fn build_timeline(store: &Store, run_id: &str) -> Result { +@@ -157,7 +157,7 @@ pub fn build_timeline(store: &Store, run_id: &str) -> Result { + + backfill_run_shas(store, run_id, &mut timeline); + Ok(RunTimeline { +- entries: timeline, ++ entries: timeline, + parallel_map: load_parallel_map(store, run_id), + }) + } +@@ -419,27 +419,27 @@ mod tests { + #[test] + fn resolve_latest_visit() { + let timeline = RunTimeline { +- entries: vec![ ++ entries: vec![ + TimelineEntry { +- ordinal: 1, +- node_name: "start".to_string(), +- visit: 1, ++ ordinal: 1, ++ node_name: "start".to_string(), ++ visit: 1, + metadata_commit_oid: Oid::zero(), +- run_commit_sha: Some("aaa".to_string()), ++ run_commit_sha: Some("aaa".to_string()), + }, + TimelineEntry { +- ordinal: 2, +- node_name: "build".to_string(), +- visit: 1, ++ ordinal: 2, ++ node_name: "build".to_string(), ++ visit: 1, + metadata_commit_oid: Oid::zero(), +- run_commit_sha: Some("bbb".to_string()), ++ run_commit_sha: Some("bbb".to_string()), + }, + TimelineEntry { +- ordinal: 3, +- node_name: "build".to_string(), +- visit: 2, ++ ordinal: 3, ++ node_name: "build".to_string(), ++ visit: 2, + metadata_commit_oid: Oid::zero(), +- run_commit_sha: Some("ccc".to_string()), ++ run_commit_sha: Some("ccc".to_string()), + }, + ], + parallel_map: HashMap::new(), +@@ -476,13 +476,13 @@ mod tests { + graph.nodes.insert("a".to_string(), a); + + graph.edges.push(fabro_graphviz::graph::Edge { +- from: "parallel1".to_string(), +- to: "a".to_string(), ++ from: "parallel1".to_string(), ++ to: "a".to_string(), + attrs: HashMap::new(), + }); + graph.edges.push(fabro_graphviz::graph::Edge { +- from: "a".to_string(), +- to: "fan_in1".to_string(), ++ from: "a".to_string(), ++ to: "fan_in1".to_string(), + attrs: HashMap::new(), + }); + +@@ -508,11 +508,14 @@ mod tests { + bs.write_entry("checkpoint.json", &cp2, "checkpoint") + .unwrap(); + +- rewind(&store, &RewindInput { +- run_id: fixtures::RUN_1, +- target: RewindTarget::Ordinal(1), +- push: false, +- }) ++ rewind( ++ &store, ++ &RewindInput { ++ run_id: fixtures::RUN_1, ++ target: RewindTarget::Ordinal(1), ++ push: false, ++ }, ++ ) + .unwrap(); + + let resolved = store.resolve_ref(&branch).unwrap().unwrap(); +diff --git a/lib/crates/fabro-workflow/src/operations/source.rs b/lib/crates/fabro-workflow/src/operations/source.rs +index 40553e27..d6728e87 100644 +--- a/lib/crates/fabro-workflow/src/operations/source.rs ++++ b/lib/crates/fabro-workflow/src/operations/source.rs +@@ -13,7 +13,7 @@ use crate::workflow_bundle::BundledWorkflow; + pub enum WorkflowInput { + Path(PathBuf), + DotSource { +- source: String, ++ source: String, + base_dir: Option, + }, + Bundled(BundledWorkflow), +@@ -23,20 +23,20 @@ pub enum WorkflowInput { + pub(crate) struct ResolveWorkflowInput { + pub workflow: WorkflowInput, + pub settings: SettingsLayer, +- pub cwd: PathBuf, ++ pub cwd: PathBuf, + } + + #[derive(Clone)] + pub(crate) struct ResolvedWorkflow { +- pub raw_source: String, +- pub settings: SettingsLayer, +- pub workflow_slug: Option, ++ pub raw_source: String, ++ pub settings: SettingsLayer, ++ pub workflow_slug: Option, + pub workflow_toml_path: Option, +- pub dot_path: Option, +- pub current_dir: Option, +- pub file_resolver: Option>, +- pub goal_override: Option, +- pub working_directory: PathBuf, ++ pub dot_path: Option, ++ pub current_dir: Option, ++ pub file_resolver: Option>, ++ pub goal_override: Option, ++ pub working_directory: PathBuf, + } + + fn workflow_slug_from_path(workflow_path: &Path) -> Option { +@@ -155,7 +155,7 @@ mod tests { + let dir = tempfile::tempdir().unwrap(); + let resolved = resolve_workflow(ResolveWorkflowInput { + workflow: WorkflowInput::DotSource { +- source: "digraph Test { start -> exit }".to_string(), ++ source: "digraph Test { start -> exit }".to_string(), + base_dir: None, + }, + settings: SettingsLayer { +@@ -165,7 +165,7 @@ mod tests { + }), + ..SettingsLayer::default() + }, +- cwd: dir.path().to_path_buf(), ++ cwd: dir.path().to_path_buf(), + }) + .unwrap(); + +diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs +index 1387852f..cfd8aaaf 100644 +--- a/lib/crates/fabro-workflow/src/operations/start.rs ++++ b/lib/crates/fabro-workflow/src/operations/start.rs +@@ -52,55 +52,55 @@ use crate::runtime_store::RunStoreHandle; + use crate::workflow_bundle::{RunDefinition, WorkflowBundle}; + + struct RunSession { +- cancel_token: Option>, +- emitter: Arc, +- sandbox: SandboxSpec, +- llm: LlmSpec, +- interviewer: Arc, +- on_node: crate::OnNodeCallback, +- lifecycle: LifecycleOptions, +- hooks: fabro_hooks::HookSettings, +- sandbox_env: SandboxEnvSpec, +- devcontainer: Option, +- seed_context: Option, +- run_store: RunStoreHandle, +- event_sink: RunEventSink, +- artifact_sink: Option, +- git: Option, +- github_app: Option, +- worktree_mode: Option, ++ cancel_token: Option>, ++ emitter: Arc, ++ sandbox: SandboxSpec, ++ llm: LlmSpec, ++ interviewer: Arc, ++ on_node: crate::OnNodeCallback, ++ lifecycle: LifecycleOptions, ++ hooks: fabro_hooks::HookSettings, ++ sandbox_env: SandboxEnvSpec, ++ devcontainer: Option, ++ seed_context: Option, ++ run_store: RunStoreHandle, ++ event_sink: RunEventSink, ++ artifact_sink: Option, ++ git: Option, ++ github_app: Option, ++ worktree_mode: Option, + registry_override: Option>, +- retro_enabled: bool, +- preserve_sandbox: bool, +- pr_config: Option, +- pr_github_app: Option, +- pr_origin_url: Option, +- pr_model: String, +- workflow_path: Option, +- workflow_bundle: Option>, +- run_control: Option>, +- vault: Option>>, ++ retro_enabled: bool, ++ preserve_sandbox: bool, ++ pr_config: Option, ++ pr_github_app: Option, ++ pr_origin_url: Option, ++ pr_model: String, ++ workflow_path: Option, ++ workflow_bundle: Option>, ++ run_control: Option>, ++ vault: Option>>, + } + + pub struct StartServices { +- pub run_id: RunId, +- pub cancel_token: Option>, +- pub emitter: Arc, +- pub interviewer: Arc, +- pub run_store: RunStoreHandle, +- pub event_sink: RunEventSink, +- pub artifact_sink: Option, +- pub run_control: Option>, +- pub github_app: Option, +- pub vault: Option>>, +- pub on_node: crate::OnNodeCallback, ++ pub run_id: RunId, ++ pub cancel_token: Option>, ++ pub emitter: Arc, ++ pub interviewer: Arc, ++ pub run_store: RunStoreHandle, ++ pub event_sink: RunEventSink, ++ pub artifact_sink: Option, ++ pub run_control: Option>, ++ pub github_app: Option, ++ pub vault: Option>>, ++ pub on_node: crate::OnNodeCallback, + pub registry_override: Option>, + } + + pub struct Started { +- pub finalized: Finalized, +- pub final_context: Option, +- pub retro: Option, ++ pub finalized: Finalized, ++ pub final_context: Option, ++ pub retro: Option, + pub retro_duration: Duration, + } + +@@ -153,9 +153,13 @@ pub(super) async fn execute_persisted_run( + .await; + return Err(error); + } +- if let Err(err) = append_event_to_sink(&event_sink, &run_id, &Event::RunStarting { +- reason: Some(StatusReason::SandboxInitializing), +- }) ++ if let Err(err) = append_event_to_sink( ++ &event_sink, ++ &run_id, ++ &Event::RunStarting { ++ reason: Some(StatusReason::SandboxInitializing), ++ }, ++ ) + .await + { + let error = Error::engine(err.to_string()); +@@ -254,12 +258,16 @@ async fn persist_terminal_engine_failure( + None, + ) + .await; +- if let Err(err) = append_event_to_sink(event_sink, &run_id, &Event::WorkflowRunFailed { +- error: error.clone(), +- duration_ms: u64::try_from(duration.as_millis()).unwrap(), +- reason: status_reason, +- git_commit_sha: None, +- }) ++ if let Err(err) = append_event_to_sink( ++ event_sink, ++ &run_id, ++ &Event::WorkflowRunFailed { ++ error: error.clone(), ++ duration_ms: u64::try_from(duration.as_millis()).unwrap(), ++ reason: status_reason, ++ git_commit_sha: None, ++ }, ++ ) + .await + { + tracing::warn!(error = %err, "Failed to append terminal engine failure event"); +@@ -278,8 +286,8 @@ impl RunSession { + .map_err(|err| Error::engine(err.to_string()))?; + let git = state.start.and_then(|start| { + start.run_branch.as_ref().map(|_| GitCheckpointOptions { +- base_sha: start.base_sha.clone(), +- run_branch: start.run_branch.clone(), ++ base_sha: start.base_sha.clone(), ++ run_branch: start.run_branch.clone(), + meta_branch: Some(MetadataStore::branch_name(&record.run_id.to_string())), + }) + }); +@@ -393,7 +401,7 @@ impl RunSession { + }; + + let devcontainer = resolved.sandbox.devcontainer.then(|| DevcontainerSpec { +- enabled: true, ++ enabled: true, + resolve_dir: working_directory.clone(), + }); + +@@ -422,9 +430,9 @@ impl RunSession { + interviewer, + on_node: services.on_node, + lifecycle: LifecycleOptions { +- setup_commands: resolved.prepare.commands.clone(), ++ setup_commands: resolved.prepare.commands.clone(), + setup_command_timeout_ms: resolved.prepare.timeout_ms, +- devcontainer_phases: Vec::new(), ++ devcontainer_phases: Vec::new(), + }, + hooks: fabro_hooks::HookSettings { + hooks: resolved.hooks.iter().map(runtime_hook_definition).collect(), +@@ -529,39 +537,39 @@ fn render_resolve_errors(errors: &[fabro_config::ResolveError]) -> String { + + fn runtime_mcp_server(settings: &ResolvedMcpServerSettings) -> McpServerSettings { + McpServerSettings { +- name: settings.name.clone(), +- transport: match &settings.transport { ++ name: settings.name.clone(), ++ transport: match &settings.transport { + ResolvedMcpTransport::Stdio { command, env } => McpTransport::Stdio { + command: command.clone(), +- env: env.clone(), ++ env: env.clone(), + }, + ResolvedMcpTransport::Http { url, headers } => McpTransport::Http { +- url: url.clone(), ++ url: url.clone(), + headers: headers.clone(), + }, + ResolvedMcpTransport::Sandbox { command, port, env } => McpTransport::Sandbox { + command: command.clone(), +- port: *port, +- env: env.clone(), ++ port: *port, ++ env: env.clone(), + }, + }, + startup_timeout_secs: settings.startup_timeout_secs, +- tool_timeout_secs: settings.tool_timeout_secs, ++ tool_timeout_secs: settings.tool_timeout_secs, + } + } + + fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { + DaytonaConfig { + auto_stop_interval: settings.auto_stop_interval, +- labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), +- snapshot: settings ++ labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()), ++ snapshot: settings + .snapshot + .as_ref() + .map(|snapshot| DaytonaSnapshotSettings { +- name: snapshot.name.clone(), +- cpu: snapshot.cpu, +- memory: snapshot.memory_gb, +- disk: snapshot.disk_gb, ++ name: snapshot.name.clone(), ++ cpu: snapshot.cpu, ++ memory: snapshot.memory_gb, ++ disk: snapshot.disk_gb, + dockerfile: snapshot + .dockerfile + .as_ref() +@@ -574,21 +582,21 @@ fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig { + } + }), + }), +- network: settings.network.as_ref().map(|network| match network { ++ network: settings.network.as_ref().map(|network| match network { + DaytonaNetworkLayer::Block => DaytonaNetwork::Block, + DaytonaNetworkLayer::AllowAll => DaytonaNetwork::AllowAll, + DaytonaNetworkLayer::AllowList { allow_list } => { + DaytonaNetwork::AllowList(allow_list.clone()) + } + }), +- skip_clone: settings.skip_clone, ++ skip_clone: settings.skip_clone, + } + } + + fn runtime_hook_definition(definition: &ResolvedHookDefinition) -> fabro_hooks::HookDefinition { + fabro_hooks::HookDefinition { +- name: definition.name.clone(), +- event: match definition.event { ++ name: definition.name.clone(), ++ event: match definition.event { + ResolvedHookEvent::RunStart => fabro_hooks::HookEvent::RunStart, + ResolvedHookEvent::RunComplete => fabro_hooks::HookEvent::RunComplete, + ResolvedHookEvent::RunFailed => fabro_hooks::HookEvent::RunFailed, +@@ -606,12 +614,12 @@ fn runtime_hook_definition(definition: &ResolvedHookDefinition) -> fabro_hooks:: + ResolvedHookEvent::PostToolUse => fabro_hooks::HookEvent::PostToolUse, + ResolvedHookEvent::PostToolUseFailure => fabro_hooks::HookEvent::PostToolUseFailure, + }, +- command: definition.command.clone(), +- hook_type: definition.hook_type.as_ref().map(runtime_hook_type), +- matcher: definition.matcher.clone(), +- blocking: definition.blocking, ++ command: definition.command.clone(), ++ hook_type: definition.hook_type.as_ref().map(runtime_hook_type), ++ matcher: definition.matcher.clone(), ++ blocking: definition.blocking, + timeout_ms: definition.timeout_ms, +- sandbox: definition.sandbox, ++ sandbox: definition.sandbox, + } + } + +@@ -626,10 +634,10 @@ fn runtime_hook_type(hook_type: &ResolvedHookType) -> fabro_hooks::HookType { + allowed_env_vars, + tls, + } => fabro_hooks::HookType::Http { +- url: url.clone(), +- headers: headers.clone(), ++ url: url.clone(), ++ headers: headers.clone(), + allowed_env_vars: allowed_env_vars.clone(), +- tls: match tls { ++ tls: match tls { + ResolvedTlsMode::Verify => fabro_hooks::TlsMode::Verify, + ResolvedTlsMode::NoVerify => fabro_hooks::TlsMode::NoVerify, + ResolvedTlsMode::Off => fabro_hooks::TlsMode::Off, +@@ -637,15 +645,15 @@ fn runtime_hook_type(hook_type: &ResolvedHookType) -> fabro_hooks::HookType { + }, + ResolvedHookType::Prompt { prompt, model } => fabro_hooks::HookType::Prompt { + prompt: prompt.clone(), +- model: model.clone(), ++ model: model.clone(), + }, + ResolvedHookType::Agent { + prompt, + model, + max_tool_rounds, + } => fabro_hooks::HookType::Agent { +- prompt: prompt.clone(), +- model: model.clone(), ++ prompt: prompt.clone(), ++ model: model.clone(), + max_tool_rounds: *max_tool_rounds, + }, + } +@@ -663,17 +671,17 @@ impl RunSession { + + let record = persisted.run_record(); + let run_options = RunOptions { +- settings: record.settings.clone(), +- run_dir: persisted.run_dir().to_path_buf(), +- cancel_token: self.cancel_token, +- run_id: record.run_id, +- labels: record.labels.clone(), +- workflow_slug: record.workflow_slug.clone(), +- github_app: self.github_app.clone(), +- host_repo_path: record.host_repo_path.as_deref().map(PathBuf::from), +- base_branch: record.base_branch.clone(), ++ settings: record.settings.clone(), ++ run_dir: persisted.run_dir().to_path_buf(), ++ cancel_token: self.cancel_token, ++ run_id: record.run_id, ++ labels: record.labels.clone(), ++ workflow_slug: record.workflow_slug.clone(), ++ github_app: self.github_app.clone(), ++ host_repo_path: record.host_repo_path.as_deref().map(PathBuf::from), ++ base_branch: record.base_branch.clone(), + display_base_sha: None, +- git: self.git.clone(), ++ git: self.git.clone(), + }; + + let last_git_sha: Arc>> = Arc::new(Mutex::new(None)); +@@ -774,21 +782,21 @@ impl RunSession { + let retro_duration = retro_start.elapsed(); + + let finalize_opts = FinalizeOptions { +- run_dir: retroed.run_options.run_dir.clone(), +- run_id: retroed.run_options.run_id, +- run_store: retroed.run_store.clone(), +- workflow_name: retroed.graph.name.clone(), +- hook_runner: retroed.hook_runner.clone(), ++ run_dir: retroed.run_options.run_dir.clone(), ++ run_id: retroed.run_options.run_id, ++ run_store: retroed.run_store.clone(), ++ workflow_name: retroed.graph.name.clone(), ++ hook_runner: retroed.hook_runner.clone(), + preserve_sandbox: self.preserve_sandbox, +- last_git_sha: last_git_sha.lock().unwrap().clone(), ++ last_git_sha: last_git_sha.lock().unwrap().clone(), + }; + let pr_opts = PullRequestOptions { +- run_dir: retroed.run_options.run_dir.clone(), +- run_store: retroed.run_store.clone(), +- pr_config: self.pr_config, ++ run_dir: retroed.run_options.run_dir.clone(), ++ run_store: retroed.run_store.clone(), ++ pr_config: self.pr_config, + github_app: self.pr_github_app, + origin_url: self.pr_origin_url, +- model: self.pr_model, ++ model: self.pr_model, + }; + + let retro = retroed.retro.clone(); +@@ -808,10 +816,10 @@ impl RunSession { + } + + struct DetachedRunBootstrapGuard { +- run_id: RunId, +- event_sink: RunEventSink, ++ run_id: RunId, ++ event_sink: RunEventSink, + cancel_token: Option>, +- active: bool, ++ active: bool, + } + + impl DetachedRunBootstrapGuard { +@@ -850,12 +858,16 @@ impl Drop for DetachedRunBootstrapGuard { + let event_sink = self.event_sink.clone(); + if let Ok(handle) = Handle::try_current() { + handle.spawn(async move { +- let _ = append_event_to_sink(&event_sink, &run_id, &Event::WorkflowRunFailed { +- error: Error::engine(format!("{reason:?}")), +- duration_ms: 0, +- reason: Some(reason), +- git_commit_sha: None, +- }) ++ let _ = append_event_to_sink( ++ &event_sink, ++ &run_id, ++ &Event::WorkflowRunFailed { ++ error: Error::engine(format!("{reason:?}")), ++ duration_ms: 0, ++ reason: Some(reason), ++ git_commit_sha: None, ++ }, ++ ) + .await; + }); + } +@@ -867,10 +879,10 @@ const POSTRUN_INTERRUPTED_MESSAGE: &str = "Run interrupted before post-run final + const POSTRUN_CANCELLED_MESSAGE: &str = "Run cancelled before post-run finalization completed."; + + struct DetachedRunCompletionGuard { +- event_sink: RunEventSink, +- run_id: RunId, ++ event_sink: RunEventSink, ++ run_id: RunId, + cancel_token: Option>, +- active: bool, ++ active: bool, + } + + impl DetachedRunCompletionGuard { +@@ -917,18 +929,26 @@ impl Drop for DetachedRunCompletionGuard { + let run_id = self.run_id; + if let Ok(handle) = Handle::try_current() { + handle.spawn(async move { +- let _ = append_event_to_sink(&event_sink, &run_id, &Event::WorkflowRunFailed { +- error: Error::engine(message.to_string()), +- duration_ms: 0, +- reason: Some(reason), +- git_commit_sha: None, +- }) ++ let _ = append_event_to_sink( ++ &event_sink, ++ &run_id, ++ &Event::WorkflowRunFailed { ++ error: Error::engine(message.to_string()), ++ duration_ms: 0, ++ reason: Some(reason), ++ git_commit_sha: None, ++ }, ++ ) + .await; +- let _ = append_event_to_sink(&event_sink, &run_id, &Event::RunNotice { +- level: RunNoticeLevel::Error, +- code: code.to_string(), +- message: message.to_string(), +- }) ++ let _ = append_event_to_sink( ++ &event_sink, ++ &run_id, ++ &Event::RunNotice { ++ level: RunNoticeLevel::Error, ++ code: code.to_string(), ++ message: message.to_string(), ++ }, ++ ) + .await; + }); + } +@@ -945,20 +965,24 @@ async fn persist_detached_failure( + ) -> Result<(), Error> { + let message = error.to_string(); + +- if let Err(err) = append_event_to_sink(event_sink, &run_id, &Event::WorkflowRunFailed { +- error: error.clone(), +- duration_ms: 0, +- reason: Some(reason), +- git_commit_sha: None, +- }) ++ if let Err(err) = append_event_to_sink( ++ event_sink, ++ &run_id, ++ &Event::WorkflowRunFailed { ++ error: error.clone(), ++ duration_ms: 0, ++ reason: Some(reason), ++ git_commit_sha: None, ++ }, ++ ) + .await + { + tracing::warn!(error = %err, "Failed to append detached failure event"); + } + + let event = Event::RunNotice { +- level: RunNoticeLevel::Error, +- code: format!("{phase}_failed"), ++ level: RunNoticeLevel::Error, ++ code: format!("{phase}_failed"), + message: message.clone(), + }; + if let Err(err) = append_event_to_sink(event_sink, &run_id, &event).await { +@@ -1011,36 +1035,39 @@ mod tests { + + async fn persisted_workflow(dot: &str, run_dir: &Path) -> (Persisted, Arc) { + let store = memory_store(); +- let created = crate::operations::create(&store, crate::operations::CreateRunInput { +- workflow: crate::operations::WorkflowInput::DotSource { +- source: dot.to_string(), +- base_dir: None, +- }, +- settings: SettingsLayer { +- run: Some(RunLayer { +- execution: Some(RunExecutionLayer { +- mode: Some(RunMode::DryRun), +- ..RunExecutionLayer::default() ++ let created = crate::operations::create( ++ &store, ++ crate::operations::CreateRunInput { ++ workflow: crate::operations::WorkflowInput::DotSource { ++ source: dot.to_string(), ++ base_dir: None, ++ }, ++ settings: SettingsLayer { ++ run: Some(RunLayer { ++ execution: Some(RunExecutionLayer { ++ mode: Some(RunMode::DryRun), ++ ..RunExecutionLayer::default() ++ }), ++ ..RunLayer::default() + }), +- ..RunLayer::default() +- }), +- ..SettingsLayer::default() ++ ..SettingsLayer::default() ++ }, ++ cwd: run_dir ++ .parent() ++ .unwrap_or_else(|| Path::new(".")) ++ .to_path_buf(), ++ workflow_slug: Some("test".to_string()), ++ workflow_path: None, ++ workflow_bundle: None, ++ submitted_manifest_bytes: None, ++ run_id: Some(fixtures::RUN_1), ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ provenance: None, ++ configured_providers: Vec::new(), + }, +- cwd: run_dir +- .parent() +- .unwrap_or_else(|| Path::new(".")) +- .to_path_buf(), +- workflow_slug: Some("test".to_string()), +- workflow_path: None, +- workflow_bundle: None, +- submitted_manifest_bytes: None, +- run_id: Some(fixtures::RUN_1), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- provenance: None, +- configured_providers: Vec::new(), +- }) ++ ) + .await + .unwrap(); + (created.persisted, store) +@@ -1159,9 +1186,11 @@ mod tests { + let registry = Arc::new(test_registry()); + let store = memory_store(); + let workflow_bundle = WorkflowBundle::new(HashMap::from([ +- (PathBuf::from("workflow.fabro"), BundledWorkflow { +- logical_path: PathBuf::from("workflow.fabro"), +- source: r#"digraph Root { ++ ( ++ PathBuf::from("workflow.fabro"), ++ BundledWorkflow { ++ logical_path: PathBuf::from("workflow.fabro"), ++ source: r#"digraph Root { + graph [goal="Bundle child"] + start [shape=Mdiamond] + manager [ +@@ -1173,50 +1202,57 @@ mod tests { + exit [shape=Msquare] + start -> manager -> exit + }"# +- .to_string(), +- files: HashMap::new(), +- }), +- (PathBuf::from("children/review.fabro"), BundledWorkflow { +- logical_path: PathBuf::from("children/review.fabro"), +- source: r"digraph Review { ++ .to_string(), ++ files: HashMap::new(), ++ }, ++ ), ++ ( ++ PathBuf::from("children/review.fabro"), ++ BundledWorkflow { ++ logical_path: PathBuf::from("children/review.fabro"), ++ source: r"digraph Review { + start [shape=Mdiamond] + exit [shape=Msquare] + start -> exit + }" +- .to_string(), +- files: HashMap::new(), +- }), ++ .to_string(), ++ files: HashMap::new(), ++ }, ++ ), + ])); + +- crate::operations::create(&store, crate::operations::CreateRunInput { +- workflow: crate::operations::WorkflowInput::Bundled( +- workflow_bundle +- .workflow(Path::new("workflow.fabro")) +- .unwrap() +- .clone(), +- ), +- settings: SettingsLayer { +- run: Some(RunLayer { +- execution: Some(RunExecutionLayer { +- mode: Some(RunMode::DryRun), +- ..RunExecutionLayer::default() ++ crate::operations::create( ++ &store, ++ crate::operations::CreateRunInput { ++ workflow: crate::operations::WorkflowInput::Bundled( ++ workflow_bundle ++ .workflow(Path::new("workflow.fabro")) ++ .unwrap() ++ .clone(), ++ ), ++ settings: SettingsLayer { ++ run: Some(RunLayer { ++ execution: Some(RunExecutionLayer { ++ mode: Some(RunMode::DryRun), ++ ..RunExecutionLayer::default() ++ }), ++ ..RunLayer::default() + }), +- ..RunLayer::default() +- }), +- ..SettingsLayer::default() ++ ..SettingsLayer::default() ++ }, ++ cwd: temp.path().to_path_buf(), ++ workflow_slug: Some("bundle-child".to_string()), ++ workflow_path: Some(PathBuf::from("workflow.fabro")), ++ workflow_bundle: Some(workflow_bundle), ++ submitted_manifest_bytes: None, ++ run_id: Some(fixtures::RUN_1), ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ provenance: None, ++ configured_providers: Vec::new(), + }, +- cwd: temp.path().to_path_buf(), +- workflow_slug: Some("bundle-child".to_string()), +- workflow_path: Some(PathBuf::from("workflow.fabro")), +- workflow_bundle: Some(workflow_bundle), +- submitted_manifest_bytes: None, +- run_id: Some(fixtures::RUN_1), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- provenance: None, +- configured_providers: Vec::new(), +- }) ++ ) + .await + .unwrap(); + +@@ -1240,15 +1276,18 @@ mod tests { + + let (_persisted, store) = persisted_workflow(MINIMAL_DOT, &run_dir).await; + +- let started = start(&run_dir, StartServices { +- on_node: Some(Arc::new({ +- let visited = Arc::clone(&visited); +- move |node_id: &str| { +- visited.lock().unwrap().push(node_id.to_string()); +- } +- })), +- ..test_start_services(&store, &run_dir, emitter, registry).await +- }) ++ let started = start( ++ &run_dir, ++ StartServices { ++ on_node: Some(Arc::new({ ++ let visited = Arc::clone(&visited); ++ move |node_id: &str| { ++ visited.lock().unwrap().push(node_id.to_string()); ++ } ++ })), ++ ..test_start_services(&store, &run_dir, emitter, registry).await ++ }, ++ ) + .await + .unwrap(); + +@@ -1268,17 +1307,17 @@ mod tests { + + // Seed an authoritative checkpoint event so start() sees it + let checkpoint = Checkpoint { +- timestamp: chrono::Utc::now(), +- current_node: "start".into(), +- completed_nodes: vec!["start".to_string()], +- node_retries: HashMap::new(), +- context_values: Context::new().snapshot(), +- node_outcomes: HashMap::new(), +- next_node_id: Some("exit".to_string()), +- git_commit_sha: None, +- loop_failure_signatures: HashMap::new(), ++ timestamp: chrono::Utc::now(), ++ current_node: "start".into(), ++ completed_nodes: vec!["start".to_string()], ++ node_retries: HashMap::new(), ++ context_values: Context::new().snapshot(), ++ node_outcomes: HashMap::new(), ++ next_node_id: Some("exit".to_string()), ++ git_commit_sha: None, ++ loop_failure_signatures: HashMap::new(), + restart_failure_signatures: HashMap::new(), +- node_visits: HashMap::new(), ++ node_visits: HashMap::new(), + }; + crate::event::append_event( + &store.open_run(&fixtures::RUN_1).await.unwrap(), +@@ -1369,51 +1408,59 @@ mod tests { + HashMap::new(), + ); + let conclusion = crate::records::Conclusion { +- timestamp: Utc::now(), +- status: StageStatus::Success, +- duration_ms: 1, +- failure_reason: None, ++ timestamp: Utc::now(), ++ status: StageStatus::Success, ++ duration_ms: 1, ++ failure_reason: None, + final_git_commit_sha: None, +- stages: vec![], +- billing: None, +- total_retries: 0, ++ stages: vec![], ++ billing: None, ++ total_retries: 0, + }; + let run_store = store.open_run(&fixtures::RUN_1).await.unwrap(); +- crate::event::append_event(&run_store, &fixtures::RUN_1, &Event::CheckpointCompleted { +- node_id: checkpoint.current_node.clone(), +- status: "success".to_string(), +- current_node: checkpoint.current_node.clone(), +- completed_nodes: checkpoint.completed_nodes.clone(), +- node_retries: checkpoint.node_retries.clone().into_iter().collect(), +- context_values: checkpoint.context_values.clone().into_iter().collect(), +- node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), +- next_node_id: checkpoint.next_node_id.clone(), +- git_commit_sha: checkpoint.git_commit_sha.clone(), +- loop_failure_signatures: checkpoint +- .loop_failure_signatures +- .iter() +- .map(|(sig, count)| (sig.to_string(), *count)) +- .collect(), +- restart_failure_signatures: checkpoint +- .restart_failure_signatures +- .iter() +- .map(|(sig, count)| (sig.to_string(), *count)) +- .collect(), +- node_visits: checkpoint.node_visits.clone().into_iter().collect(), +- diff: None, +- }) ++ crate::event::append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::CheckpointCompleted { ++ node_id: checkpoint.current_node.clone(), ++ status: "success".to_string(), ++ current_node: checkpoint.current_node.clone(), ++ completed_nodes: checkpoint.completed_nodes.clone(), ++ node_retries: checkpoint.node_retries.clone().into_iter().collect(), ++ context_values: checkpoint.context_values.clone().into_iter().collect(), ++ node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), ++ next_node_id: checkpoint.next_node_id.clone(), ++ git_commit_sha: checkpoint.git_commit_sha.clone(), ++ loop_failure_signatures: checkpoint ++ .loop_failure_signatures ++ .iter() ++ .map(|(sig, count)| (sig.to_string(), *count)) ++ .collect(), ++ restart_failure_signatures: checkpoint ++ .restart_failure_signatures ++ .iter() ++ .map(|(sig, count)| (sig.to_string(), *count)) ++ .collect(), ++ node_visits: checkpoint.node_visits.clone().into_iter().collect(), ++ diff: None, ++ }, ++ ) + .await + .unwrap(); +- crate::event::append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted { +- duration_ms: conclusion.duration_ms, +- artifact_count: 0, +- status: "success".to_string(), +- reason: None, +- total_usd_micros: None, +- final_git_commit_sha: None, +- final_patch: None, +- billing: None, +- }) ++ crate::event::append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::WorkflowRunCompleted { ++ duration_ms: conclusion.duration_ms, ++ artifact_count: 0, ++ status: "success".to_string(), ++ reason: None, ++ total_usd_micros: None, ++ final_git_commit_sha: None, ++ final_patch: None, ++ billing: None, ++ }, ++ ) + .await + .unwrap(); + +diff --git a/lib/crates/fabro-workflow/src/operations/validate.rs b/lib/crates/fabro-workflow/src/operations/validate.rs +index 83007660..05597e56 100644 +--- a/lib/crates/fabro-workflow/src/operations/validate.rs ++++ b/lib/crates/fabro-workflow/src/operations/validate.rs +@@ -9,9 +9,9 @@ use crate::pipeline::Validated; + use crate::transforms::Transform; + + pub struct ValidateInput { +- pub workflow: WorkflowInput, +- pub settings: SettingsLayer, +- pub cwd: PathBuf, ++ pub workflow: WorkflowInput, ++ pub settings: SettingsLayer, ++ pub cwd: PathBuf, + pub custom_transforms: Vec>, + } + +@@ -23,7 +23,7 @@ pub fn validate(input: ValidateInput) -> Result { + let resolved = resolve_workflow(ResolveWorkflowInput { + workflow: input.workflow, + settings: input.settings, +- cwd: input.cwd, ++ cwd: input.cwd, + }) + .map_err(|err| Error::Parse(err.to_string()))?; + +diff --git a/lib/crates/fabro-workflow/src/outcome.rs b/lib/crates/fabro-workflow/src/outcome.rs +index ada74ab7..d5c1daf1 100644 +--- a/lib/crates/fabro-workflow/src/outcome.rs ++++ b/lib/crates/fabro-workflow/src/outcome.rs +@@ -170,10 +170,10 @@ mod tests { + #[test] + fn billed_model_usage_from_llm_bills_anthropic_fast_mode_cache_write_pricing() { + let usage = TokenCounts { +- input_tokens: 100_000, +- output_tokens: 10_000, +- reasoning_tokens: 5_000, +- cache_read_tokens: 20_000, ++ input_tokens: 100_000, ++ output_tokens: 10_000, ++ reasoning_tokens: 5_000, ++ cache_read_tokens: 20_000, + cache_write_tokens: 30_000, + }; + let billed = billed_model_usage_from_llm( +@@ -189,10 +189,10 @@ mod tests { + #[test] + fn billed_model_usage_round_trips_dense_token_counts() { + let usage = TokenCounts { +- input_tokens: 100, +- output_tokens: 40, +- reasoning_tokens: 5, +- cache_read_tokens: 20, ++ input_tokens: 100, ++ output_tokens: 40, ++ reasoning_tokens: 5, ++ cache_read_tokens: 20, + cache_write_tokens: 10, + }; + let billed = +diff --git a/lib/crates/fabro-workflow/src/pipeline/execute.rs b/lib/crates/fabro-workflow/src/pipeline/execute.rs +index 79f74a3c..1d305030 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/execute.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/execute.rs +@@ -99,8 +99,8 @@ pub async fn execute(init: Initialized) -> Executed { + + let handler = Arc::new(WorkflowNodeHandler { + services: shared_services, +- run_dir: run_options.run_dir.clone(), +- graph: Arc::clone(&graph_arc), ++ run_dir: run_options.run_dir.clone(), ++ graph: Arc::clone(&graph_arc), + }); + + let settings_arc = Arc::new(run_options.clone()); +@@ -315,7 +315,7 @@ pub async fn execute(init: Initialized) -> Executed { + let stall_timeout = graph.stall_timeout().unwrap_or_default(); + let idle_secs = stall_timeout.as_secs(); + emitter.emit(&Event::StallWatchdogTimeout { +- node: node_id.clone(), ++ node: node_id.clone(), + idle_seconds: idle_secs, + }); + ( +diff --git a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +index 3320d8bc..c63b0666 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/execute/tests.rs +@@ -88,17 +88,17 @@ fn test_emitter_arc(label: &str) -> Arc { + + fn test_run_options(run_dir: &Path, run_id: &str) -> RunOptions { + RunOptions { +- run_dir: run_dir.to_path_buf(), +- cancel_token: None, +- run_id: test_run_id(run_id), +- settings: SettingsLayer::default(), +- git: None, +- host_repo_path: None, +- labels: HashMap::new(), +- github_app: None, +- base_branch: None, ++ run_dir: run_dir.to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id(run_id), ++ settings: SettingsLayer::default(), ++ git: None, ++ host_repo_path: None, ++ labels: HashMap::new(), ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- workflow_slug: None, ++ workflow_slug: None, + } + } + +@@ -197,27 +197,27 @@ async fn execute_test_run_with_options( + working_directory: std::env::current_dir().unwrap(), + }, + llm: LlmSpec { +- model: "test-model".to_string(), +- provider: fabro_llm::Provider::Anthropic, ++ model: "test-model".to_string(), ++ provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), +- mcp_servers: Vec::new(), +- dry_run: true, ++ mcp_servers: Vec::new(), ++ dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle: LifecycleOptions { +- setup_commands: vec![], ++ setup_commands: vec![], + setup_command_timeout_ms: 1_000, +- devcontainer_phases: vec![], ++ devcontainer_phases: vec![], + }, + run_options, + workflow_path: None, + workflow_bundle: None, + hooks: HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { +- devcontainer_env: HashMap::new(), +- toml_env: HashMap::new(), ++ devcontainer_env: HashMap::new(), ++ toml_env: HashMap::new(), + github_permissions: None, +- origin_url: None, ++ origin_url: None, + }, + vault: None, + devcontainer: None, +@@ -247,45 +247,45 @@ async fn execute_runs_start_to_exit_and_returns_final_context() { + let initialized = initialize( + persisted_workflow(graph, source, &run_dir, test_run_id("run-test")), + InitOptions { +- run_id: test_run_id("run-test"), +- run_store: test_run_store(&test_run_id("run-test")).await.into(), +- dry_run: false, +- emitter: test_emitter_arc("run-test"), +- sandbox: SandboxSpec::Local { ++ run_id: test_run_id("run-test"), ++ run_store: test_run_store(&test_run_id("run-test")).await.into(), ++ dry_run: false, ++ emitter: test_emitter_arc("run-test"), ++ sandbox: SandboxSpec::Local { + working_directory: std::env::current_dir().unwrap(), + }, +- llm: LlmSpec { +- model: "test-model".to_string(), +- provider: fabro_llm::Provider::Anthropic, ++ llm: LlmSpec { ++ model: "test-model".to_string(), ++ provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), +- mcp_servers: Vec::new(), +- dry_run: true, ++ mcp_servers: Vec::new(), ++ dry_run: true, + }, +- interviewer: Arc::new(AutoApproveInterviewer), +- lifecycle: LifecycleOptions { +- setup_commands: vec![], ++ interviewer: Arc::new(AutoApproveInterviewer), ++ lifecycle: LifecycleOptions { ++ setup_commands: vec![], + setup_command_timeout_ms: 1_000, +- devcontainer_phases: vec![], ++ devcontainer_phases: vec![], + }, +- run_options: test_run_options(&run_dir, "run-test"), +- workflow_path: None, +- workflow_bundle: None, +- hooks: HookSettings { hooks: vec![] }, +- sandbox_env: SandboxEnvSpec { +- devcontainer_env: HashMap::new(), +- toml_env: HashMap::new(), ++ run_options: test_run_options(&run_dir, "run-test"), ++ workflow_path: None, ++ workflow_bundle: None, ++ hooks: HookSettings { hooks: vec![] }, ++ sandbox_env: SandboxEnvSpec { ++ devcontainer_env: HashMap::new(), ++ toml_env: HashMap::new(), + github_permissions: None, +- origin_url: None, ++ origin_url: None, + }, +- vault: None, +- devcontainer: None, +- git: None, +- worktree_mode: None, +- run_control: None, ++ vault: None, ++ devcontainer: None, ++ git: None, ++ worktree_mode: None, ++ run_control: None, + registry_override: None, +- artifact_sink: None, +- checkpoint: None, +- seed_context: None, ++ artifact_sink: None, ++ checkpoint: None, ++ seed_context: None, + }, + ) + .await +@@ -327,11 +327,11 @@ async fn run_with_lifecycle( + working_directory: PathBuf::from(sandbox.working_directory()), + }, + llm: LlmSpec { +- model: "test-model".to_string(), +- provider: fabro_llm::Provider::Anthropic, ++ model: "test-model".to_string(), ++ provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), +- mcp_servers: Vec::new(), +- dry_run: true, ++ mcp_servers: Vec::new(), ++ dry_run: true, + }, + interviewer: Arc::new(AutoApproveInterviewer), + lifecycle, +@@ -340,10 +340,10 @@ async fn run_with_lifecycle( + workflow_bundle: None, + hooks: HookSettings { hooks: vec![] }, + sandbox_env: SandboxEnvSpec { +- devcontainer_env: HashMap::new(), +- toml_env: HashMap::new(), ++ devcontainer_env: HashMap::new(), ++ toml_env: HashMap::new(), + github_permissions: None, +- origin_url: None, ++ origin_url: None, + }, + vault: None, + devcontainer: None, +@@ -645,8 +645,8 @@ async fn execute_writes_start_json_and_node_status() { + let dir = tempfile::tempdir().unwrap(); + let mut run_options = test_run_options(dir.path(), "test-run"); + run_options.git = Some(GitCheckpointOptions { +- base_sha: Some("abc123".into()), +- run_branch: Some(format!("fabro/run/{}", test_run_id("test-run"))), ++ base_sha: Some("abc123".into()), ++ run_branch: Some(format!("fabro/run/{}", test_run_id("test-run"))), + meta_branch: None, + }); + +diff --git a/lib/crates/fabro-workflow/src/pipeline/finalize.rs b/lib/crates/fabro-workflow/src/pipeline/finalize.rs +index bd9ed3d3..bc4edf2f 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/finalize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/finalize.rs +@@ -319,17 +319,17 @@ mod tests { + + fn test_run_options(run_dir: &std::path::Path) -> RunOptions { + RunOptions { +- settings: SettingsLayer::default(), +- run_dir: run_dir.to_path_buf(), +- cancel_token: None, +- run_id: test_run_id(), +- labels: HashMap::new(), +- workflow_slug: None, +- github_app: None, +- host_repo_path: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: run_dir.to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id(), ++ labels: HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ host_repo_path: None, ++ base_branch: None, + display_base_sha: None, +- git: None, ++ git: None, + } + } + +@@ -366,15 +366,18 @@ mod tests { + retro: None, + }; + +- let concluded = finalize(retroed, &FinalizeOptions { +- run_dir: run_dir.clone(), +- run_id: test_run_id(), +- run_store: run_store.clone().into(), +- workflow_name: "test".to_string(), +- hook_runner: None, +- preserve_sandbox: true, +- last_git_sha: None, +- }) ++ let concluded = finalize( ++ retroed, ++ &FinalizeOptions { ++ run_dir: run_dir.clone(), ++ run_id: test_run_id(), ++ run_store: run_store.clone().into(), ++ workflow_name: "test".to_string(), ++ hook_runner: None, ++ preserve_sandbox: true, ++ last_git_sha: None, ++ }, ++ ) + .await + .unwrap(); + store_logger.flush().await; +diff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs +index dd4c13cc..f41ce10f 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs +@@ -32,9 +32,9 @@ use crate::handler::{HandlerRegistry, default_registry, sandbox_cancel_token}; + use crate::run_options::GitCheckpointOptions; + + struct WorktreePlan { +- branch_name: String, +- base_sha: String, +- worktree_path: PathBuf, ++ branch_name: String, ++ base_sha: String, ++ worktree_path: PathBuf, + skip_branch_creation: bool, + } + +@@ -74,10 +74,9 @@ async fn resolve_worktree_plan(options: &mut InitOptions) -> Result { +@@ -571,11 +573,11 @@ pub async fn initialize( + + let sandbox_record = options.sandbox.to_sandbox_record(&*sandbox); + options.emitter.emit(&Event::SandboxInitialized { +- working_directory: sandbox_record.working_directory.clone(), +- provider: sandbox_record.provider.clone(), +- identifier: sandbox_record.identifier.clone(), ++ working_directory: sandbox_record.working_directory.clone(), ++ provider: sandbox_record.provider.clone(), ++ identifier: sandbox_record.identifier.clone(), + host_working_directory: sandbox_record.host_working_directory.clone(), +- container_mount_point: sandbox_record.container_mount_point.clone(), ++ container_mount_point: sandbox_record.container_mount_point.clone(), + }); + + let env = build_sandbox_env( +@@ -844,17 +846,17 @@ mod tests { + + fn test_settings(run_dir: &std::path::Path) -> RunOptions { + RunOptions { +- settings: SettingsLayer::default(), +- run_dir: run_dir.to_path_buf(), +- cancel_token: None, +- run_id: test_run_id(), +- labels: HashMap::new(), +- workflow_slug: None, +- github_app: None, +- host_repo_path: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: run_dir.to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id(), ++ labels: HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ host_repo_path: None, ++ base_branch: None, + display_base_sha: None, +- git: None, ++ git: None, + } + } + +@@ -890,51 +892,54 @@ mod tests { + let persisted = test_persisted(graph, source.clone(), &run_dir); + let emitter = Arc::new(crate::event::Emitter::new(test_run_id())); + +- let initialized = initialize(persisted, InitOptions { +- run_id: test_run_id(), +- run_store: { +- let store = memory_store(); +- let inner = store.create_run(&test_run_id()).await.unwrap(); +- inner.into() +- }, +- dry_run: false, +- emitter, +- sandbox: SandboxSpec::Local { +- working_directory: std::env::current_dir().unwrap(), +- }, +- llm: LlmSpec { +- model: "test-model".to_string(), +- provider: fabro_llm::Provider::Anthropic, +- fallback_chain: Vec::new(), +- mcp_servers: Vec::new(), +- dry_run: true, +- }, +- interviewer: Arc::new(AutoApproveInterviewer), +- lifecycle: crate::run_options::LifecycleOptions { +- setup_commands: vec![], +- setup_command_timeout_ms: 1_000, +- devcontainer_phases: vec![], +- }, +- run_options: test_settings(&run_dir), +- workflow_path: None, +- workflow_bundle: None, +- hooks: fabro_hooks::HookSettings { hooks: vec![] }, +- sandbox_env: SandboxEnvSpec { +- devcontainer_env: HashMap::new(), +- toml_env: HashMap::from([("TEST_KEY".to_string(), "value".to_string())]), +- github_permissions: None, +- origin_url: None, ++ let initialized = initialize( ++ persisted, ++ InitOptions { ++ run_id: test_run_id(), ++ run_store: { ++ let store = memory_store(); ++ let inner = store.create_run(&test_run_id()).await.unwrap(); ++ inner.into() ++ }, ++ dry_run: false, ++ emitter, ++ sandbox: SandboxSpec::Local { ++ working_directory: std::env::current_dir().unwrap(), ++ }, ++ llm: LlmSpec { ++ model: "test-model".to_string(), ++ provider: fabro_llm::Provider::Anthropic, ++ fallback_chain: Vec::new(), ++ mcp_servers: Vec::new(), ++ dry_run: true, ++ }, ++ interviewer: Arc::new(AutoApproveInterviewer), ++ lifecycle: crate::run_options::LifecycleOptions { ++ setup_commands: vec![], ++ setup_command_timeout_ms: 1_000, ++ devcontainer_phases: vec![], ++ }, ++ run_options: test_settings(&run_dir), ++ workflow_path: None, ++ workflow_bundle: None, ++ hooks: fabro_hooks::HookSettings { hooks: vec![] }, ++ sandbox_env: SandboxEnvSpec { ++ devcontainer_env: HashMap::new(), ++ toml_env: HashMap::from([("TEST_KEY".to_string(), "value".to_string())]), ++ github_permissions: None, ++ origin_url: None, ++ }, ++ vault: None, ++ devcontainer: None, ++ git: None, ++ worktree_mode: None, ++ run_control: None, ++ registry_override: None, ++ artifact_sink: None, ++ checkpoint: None, ++ seed_context: None, + }, +- vault: None, +- devcontainer: None, +- git: None, +- worktree_mode: None, +- run_control: None, +- registry_override: None, +- artifact_sink: None, +- checkpoint: None, +- seed_context: None, +- }) ++ ) + .await + .unwrap(); + +@@ -960,7 +965,7 @@ mod tests { + "anthropic", + &serde_json::to_string(&AuthCredential { + provider: fabro_llm::Provider::Anthropic, +- details: AuthDetails::ApiKey { ++ details: AuthDetails::ApiKey { + key: "anthropic-key".to_string(), + }, + }) +@@ -973,11 +978,11 @@ mod tests { + + let (_, llm_client, effective_dry_run) = build_registry( + &LlmSpec { +- model: "claude-opus-4-6".to_string(), +- provider: fabro_llm::Provider::Anthropic, ++ model: "claude-opus-4-6".to_string(), ++ provider: fabro_llm::Provider::Anthropic, + fallback_chain: Vec::new(), +- mcp_servers: Vec::new(), +- dry_run: false, ++ mcp_servers: Vec::new(), ++ dry_run: false, + }, + Arc::new(AutoApproveInterviewer), + &HashMap::new(), +@@ -1009,47 +1014,50 @@ mod tests { + }); + store_logger.register(&emitter); + +- let initialized = initialize(persisted, InitOptions { +- run_id: test_run_id(), +- run_store: run_store.into(), +- dry_run: false, +- emitter, +- sandbox: SandboxSpec::Local { +- working_directory: std::env::current_dir().unwrap(), +- }, +- llm: LlmSpec { +- model: "test-model".to_string(), +- provider: fabro_llm::Provider::Anthropic, +- fallback_chain: Vec::new(), +- mcp_servers: Vec::new(), +- dry_run: true, +- }, +- interviewer: Arc::new(AutoApproveInterviewer), +- lifecycle: crate::run_options::LifecycleOptions { +- setup_commands: vec!["true".to_string()], +- setup_command_timeout_ms: 1_000, +- devcontainer_phases: vec![], +- }, +- run_options: test_settings(&run_dir), +- workflow_path: None, +- workflow_bundle: None, +- hooks: fabro_hooks::HookSettings { hooks: vec![] }, +- sandbox_env: SandboxEnvSpec { +- devcontainer_env: HashMap::new(), +- toml_env: HashMap::new(), +- github_permissions: None, +- origin_url: None, ++ let initialized = initialize( ++ persisted, ++ InitOptions { ++ run_id: test_run_id(), ++ run_store: run_store.into(), ++ dry_run: false, ++ emitter, ++ sandbox: SandboxSpec::Local { ++ working_directory: std::env::current_dir().unwrap(), ++ }, ++ llm: LlmSpec { ++ model: "test-model".to_string(), ++ provider: fabro_llm::Provider::Anthropic, ++ fallback_chain: Vec::new(), ++ mcp_servers: Vec::new(), ++ dry_run: true, ++ }, ++ interviewer: Arc::new(AutoApproveInterviewer), ++ lifecycle: crate::run_options::LifecycleOptions { ++ setup_commands: vec!["true".to_string()], ++ setup_command_timeout_ms: 1_000, ++ devcontainer_phases: vec![], ++ }, ++ run_options: test_settings(&run_dir), ++ workflow_path: None, ++ workflow_bundle: None, ++ hooks: fabro_hooks::HookSettings { hooks: vec![] }, ++ sandbox_env: SandboxEnvSpec { ++ devcontainer_env: HashMap::new(), ++ toml_env: HashMap::new(), ++ github_permissions: None, ++ origin_url: None, ++ }, ++ vault: None, ++ devcontainer: None, ++ git: None, ++ worktree_mode: None, ++ run_control: None, ++ registry_override: None, ++ artifact_sink: None, ++ checkpoint: None, ++ seed_context: None, + }, +- vault: None, +- devcontainer: None, +- git: None, +- worktree_mode: None, +- run_control: None, +- registry_override: None, +- artifact_sink: None, +- checkpoint: None, +- seed_context: None, +- }) ++ ) + .await + .unwrap(); + store_logger.flush().await; +@@ -1074,51 +1082,54 @@ mod tests { + let mut run_options = test_settings(&run_dir); + run_options.cancel_token = Some(cancel_token); + +- let result = initialize(persisted, InitOptions { +- run_id: test_run_id(), +- run_store: { +- let store = memory_store(); +- let inner = store.create_run(&test_run_id()).await.unwrap(); +- inner.into() +- }, +- dry_run: false, +- emitter: Arc::new(crate::event::Emitter::new(test_run_id())), +- sandbox: SandboxSpec::Local { +- working_directory: std::env::current_dir().unwrap(), +- }, +- llm: LlmSpec { +- model: "test-model".to_string(), +- provider: fabro_llm::Provider::Anthropic, +- fallback_chain: Vec::new(), +- mcp_servers: Vec::new(), +- dry_run: true, +- }, +- interviewer: Arc::new(AutoApproveInterviewer), +- lifecycle: crate::run_options::LifecycleOptions { +- setup_commands: vec!["sleep 5".to_string()], +- setup_command_timeout_ms: 5_000, +- devcontainer_phases: vec![], +- }, +- run_options, +- workflow_path: None, +- workflow_bundle: None, +- hooks: fabro_hooks::HookSettings { hooks: vec![] }, +- sandbox_env: SandboxEnvSpec { +- devcontainer_env: HashMap::new(), +- toml_env: HashMap::new(), +- github_permissions: None, +- origin_url: None, ++ let result = initialize( ++ persisted, ++ InitOptions { ++ run_id: test_run_id(), ++ run_store: { ++ let store = memory_store(); ++ let inner = store.create_run(&test_run_id()).await.unwrap(); ++ inner.into() ++ }, ++ dry_run: false, ++ emitter: Arc::new(crate::event::Emitter::new(test_run_id())), ++ sandbox: SandboxSpec::Local { ++ working_directory: std::env::current_dir().unwrap(), ++ }, ++ llm: LlmSpec { ++ model: "test-model".to_string(), ++ provider: fabro_llm::Provider::Anthropic, ++ fallback_chain: Vec::new(), ++ mcp_servers: Vec::new(), ++ dry_run: true, ++ }, ++ interviewer: Arc::new(AutoApproveInterviewer), ++ lifecycle: crate::run_options::LifecycleOptions { ++ setup_commands: vec!["sleep 5".to_string()], ++ setup_command_timeout_ms: 5_000, ++ devcontainer_phases: vec![], ++ }, ++ run_options, ++ workflow_path: None, ++ workflow_bundle: None, ++ hooks: fabro_hooks::HookSettings { hooks: vec![] }, ++ sandbox_env: SandboxEnvSpec { ++ devcontainer_env: HashMap::new(), ++ toml_env: HashMap::new(), ++ github_permissions: None, ++ origin_url: None, ++ }, ++ vault: None, ++ devcontainer: None, ++ git: None, ++ worktree_mode: None, ++ run_control: None, ++ registry_override: None, ++ artifact_sink: None, ++ checkpoint: None, ++ seed_context: None, + }, +- vault: None, +- devcontainer: None, +- git: None, +- worktree_mode: None, +- run_control: None, +- registry_override: None, +- artifact_sink: None, +- checkpoint: None, +- seed_context: None, +- }) ++ ) + .await; + + assert!(matches!(result, Err(Error::Cancelled))); +@@ -1135,53 +1146,57 @@ mod tests { + let mut run_options = test_settings(&run_dir); + run_options.cancel_token = Some(cancel_token); + +- let result = initialize(persisted, InitOptions { +- run_id: test_run_id(), +- run_store: { +- let store = memory_store(); +- let inner = store.create_run(&test_run_id()).await.unwrap(); +- inner.into() +- }, +- dry_run: false, +- emitter: Arc::new(crate::event::Emitter::new(test_run_id())), +- sandbox: SandboxSpec::Local { +- working_directory: std::env::current_dir().unwrap(), +- }, +- llm: LlmSpec { +- model: "test-model".to_string(), +- provider: fabro_llm::Provider::Anthropic, +- fallback_chain: Vec::new(), +- mcp_servers: Vec::new(), +- dry_run: true, +- }, +- interviewer: Arc::new(AutoApproveInterviewer), +- lifecycle: crate::run_options::LifecycleOptions { +- setup_commands: vec![], +- setup_command_timeout_ms: 5_000, +- devcontainer_phases: vec![("on_create".to_string(), vec![ +- fabro_devcontainer::Command::Shell("sleep 5".to_string()), +- ])], +- }, +- run_options, +- workflow_path: None, +- workflow_bundle: None, +- hooks: fabro_hooks::HookSettings { hooks: vec![] }, +- sandbox_env: SandboxEnvSpec { +- devcontainer_env: HashMap::new(), +- toml_env: HashMap::new(), +- github_permissions: None, +- origin_url: None, ++ let result = initialize( ++ persisted, ++ InitOptions { ++ run_id: test_run_id(), ++ run_store: { ++ let store = memory_store(); ++ let inner = store.create_run(&test_run_id()).await.unwrap(); ++ inner.into() ++ }, ++ dry_run: false, ++ emitter: Arc::new(crate::event::Emitter::new(test_run_id())), ++ sandbox: SandboxSpec::Local { ++ working_directory: std::env::current_dir().unwrap(), ++ }, ++ llm: LlmSpec { ++ model: "test-model".to_string(), ++ provider: fabro_llm::Provider::Anthropic, ++ fallback_chain: Vec::new(), ++ mcp_servers: Vec::new(), ++ dry_run: true, ++ }, ++ interviewer: Arc::new(AutoApproveInterviewer), ++ lifecycle: crate::run_options::LifecycleOptions { ++ setup_commands: vec![], ++ setup_command_timeout_ms: 5_000, ++ devcontainer_phases: vec![( ++ "on_create".to_string(), ++ vec![fabro_devcontainer::Command::Shell("sleep 5".to_string())], ++ )], ++ }, ++ run_options, ++ workflow_path: None, ++ workflow_bundle: None, ++ hooks: fabro_hooks::HookSettings { hooks: vec![] }, ++ sandbox_env: SandboxEnvSpec { ++ devcontainer_env: HashMap::new(), ++ toml_env: HashMap::new(), ++ github_permissions: None, ++ origin_url: None, ++ }, ++ vault: None, ++ devcontainer: None, ++ git: None, ++ worktree_mode: None, ++ run_control: None, ++ registry_override: None, ++ artifact_sink: None, ++ checkpoint: None, ++ seed_context: None, + }, +- vault: None, +- devcontainer: None, +- git: None, +- worktree_mode: None, +- run_control: None, +- registry_override: None, +- artifact_sink: None, +- checkpoint: None, +- seed_context: None, +- }) ++ ) + .await; + + assert!(matches!(result, Err(Error::Cancelled))); +diff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs +index 523abf0b..a947b05e 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/persist.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs +@@ -158,23 +158,27 @@ mod tests { + async fn seeded_store(run_dir: &Path, record: &RunRecord, source: Option<&str>) -> RunDatabase { + let store = memory_store(); + let run_store = store.create_run(&record.run_id).await.unwrap(); +- append_event(&run_store, &record.run_id, &Event::RunCreated { +- run_id: record.run_id, +- settings: serde_json::to_value(&record.settings).unwrap(), +- graph: serde_json::to_value(&record.graph).unwrap(), +- workflow_source: source.map(ToOwned::to_owned), +- workflow_config: None, +- labels: record.labels.clone().into_iter().collect(), +- run_dir: run_dir.to_string_lossy().to_string(), +- working_directory: record.working_directory.display().to_string(), +- host_repo_path: record.host_repo_path.clone(), +- repo_origin_url: record.repo_origin_url.clone(), +- base_branch: record.base_branch.clone(), +- workflow_slug: record.workflow_slug.clone(), +- db_prefix: None, +- provenance: record.provenance.clone(), +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &record.run_id, ++ &Event::RunCreated { ++ run_id: record.run_id, ++ settings: serde_json::to_value(&record.settings).unwrap(), ++ graph: serde_json::to_value(&record.graph).unwrap(), ++ workflow_source: source.map(ToOwned::to_owned), ++ workflow_config: None, ++ labels: record.labels.clone().into_iter().collect(), ++ run_dir: run_dir.to_string_lossy().to_string(), ++ working_directory: record.working_directory.display().to_string(), ++ host_repo_path: record.host_repo_path.clone(), ++ repo_origin_url: record.repo_origin_url.clone(), ++ base_branch: record.base_branch.clone(), ++ workflow_slug: record.workflow_slug.clone(), ++ db_prefix: None, ++ provenance: record.provenance.clone(), ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); + run_store +@@ -188,7 +192,7 @@ mod tests { + let persisted = persist( + Validated::new(graph.clone(), source, vec![]), + PersistOptions { +- run_dir: run_dir.clone(), ++ run_dir: run_dir.clone(), + run_record: sample_record(different_graph()), + }, + ) +@@ -215,7 +219,7 @@ mod tests { + let persisted = persist( + Validated::new(graph.clone(), source, vec![]), + PersistOptions { +- run_dir: run_dir.clone(), ++ run_dir: run_dir.clone(), + run_record: sample_record(different_graph()), + }, + ) +@@ -240,7 +244,7 @@ mod tests { + persist( + Validated::new(graph, source.clone(), vec![]), + PersistOptions { +- run_dir: run_dir.clone(), ++ run_dir: run_dir.clone(), + run_record: expected.clone(), + }, + ) +@@ -280,10 +284,13 @@ mod tests { + std::fs::write(&run_dir, "not a directory").unwrap(); + let (graph, source) = graph_and_source(); + +- let err = persist(Validated::new(graph, source, vec![]), PersistOptions { +- run_dir, +- run_record: sample_record(different_graph()), +- }) ++ let err = persist( ++ Validated::new(graph, source, vec![]), ++ PersistOptions { ++ run_dir, ++ run_record: sample_record(different_graph()), ++ }, ++ ) + .unwrap_err(); + + assert!(matches!(err, Error::Io(_))); +diff --git a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +index a44cf7b1..edbf3f95 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/pull_request.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/pull_request.rs +@@ -540,14 +540,14 @@ pub async fn pull_request(concluded: Concluded, options: &PullRequestOptions) -> + { + Ok(Some(record)) => { + emitter.emit(&Event::PullRequestCreated { +- pr_url: record.html_url.clone(), +- pr_number: record.number, +- owner: record.owner.clone(), +- repo: record.repo.clone(), ++ pr_url: record.html_url.clone(), ++ pr_number: record.number, ++ owner: record.owner.clone(), ++ repo: record.repo.clone(), + base_branch: record.base_branch.clone(), + head_branch: record.head_branch.clone(), +- title: record.title.clone(), +- draft: pr_cfg.draft, ++ title: record.title.clone(), ++ draft: pr_cfg.draft, + }); + pr_url = Some(record.html_url.clone()); + } +@@ -622,19 +622,19 @@ mod tests { + + async fn complete(&self, _request: &Request) -> Result { + Ok(Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(&self.response_text), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(&self.response_text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 20, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }) + } + +@@ -650,19 +650,19 @@ mod tests { + ..Default::default() + }, + Response { +- id: "resp_1".into(), +- model: "mock-model".into(), +- provider: "mock".into(), +- message: Message::assistant(&text), ++ id: "resp_1".into(), ++ model: "mock-model".into(), ++ provider: "mock".into(), ++ message: Message::assistant(&text), + finish_reason: FinishReason::Stop, +- usage: TokenCounts { ++ usage: TokenCounts { + input_tokens: 10, + output_tokens: 20, + ..Default::default() + }, +- raw: None, +- warnings: vec![], +- rate_limit: None, ++ raw: None, ++ warnings: vec![], ++ rate_limit: None, + }, + )), + ]; +@@ -694,109 +694,109 @@ mod tests { + + fn make_test_conclusion() -> Conclusion { + Conclusion { +- timestamp: Utc::now(), +- status: crate::outcome::StageStatus::Success, +- duration_ms: 150_000, +- failure_reason: None, ++ timestamp: Utc::now(), ++ status: crate::outcome::StageStatus::Success, ++ duration_ms: 150_000, ++ failure_reason: None, + final_git_commit_sha: None, +- stages: vec![ ++ stages: vec![ + StageSummary { +- stage_id: "plan".to_string(), +- stage_label: "plan".to_string(), +- duration_ms: 45_000, ++ stage_id: "plan".to_string(), ++ stage_label: "plan".to_string(), ++ duration_ms: 45_000, + billing_usd_micros: Some(120_000), +- retries: 0, ++ retries: 0, + }, + StageSummary { +- stage_id: "implement".to_string(), +- stage_label: "implement".to_string(), +- duration_ms: 90_000, ++ stage_id: "implement".to_string(), ++ stage_label: "implement".to_string(), ++ duration_ms: 90_000, + billing_usd_micros: Some(250_000), +- retries: 0, ++ retries: 0, + }, + StageSummary { +- stage_id: "simplify".to_string(), +- stage_label: "simplify".to_string(), +- duration_ms: 15_000, ++ stage_id: "simplify".to_string(), ++ stage_label: "simplify".to_string(), ++ duration_ms: 15_000, + billing_usd_micros: Some(50_000), +- retries: 0, ++ retries: 0, + }, + ], +- billing: Some(BilledTokenCounts { ++ billing: Some(BilledTokenCounts { + total_usd_micros: Some(420_000), + ..BilledTokenCounts::default() + }), +- total_retries: 0, ++ total_retries: 0, + } + } + + fn make_test_retro() -> Retro { + Retro { +- run_id: fixtures::RUN_1, +- workflow_name: "implement".to_string(), +- goal: "Fix the bug".to_string(), +- timestamp: Utc::now(), +- smoothness: None, +- stages: vec![ ++ run_id: fixtures::RUN_1, ++ workflow_name: "implement".to_string(), ++ goal: "Fix the bug".to_string(), ++ timestamp: Utc::now(), ++ smoothness: None, ++ stages: vec![ + StageRetro { +- stage_id: "plan".to_string(), +- stage_label: "plan".to_string(), +- status: "success".to_string(), +- duration_ms: 45_000, +- retries: 0, ++ stage_id: "plan".to_string(), ++ stage_label: "plan".to_string(), ++ status: "success".to_string(), ++ duration_ms: 45_000, ++ retries: 0, + billing_usd_micros: Some(120_000), +- notes: None, +- failure_reason: None, +- files_touched: vec![], ++ notes: None, ++ failure_reason: None, ++ files_touched: vec![], + }, + StageRetro { +- stage_id: "implement".to_string(), +- stage_label: "implement".to_string(), +- status: "success".to_string(), +- duration_ms: 90_000, +- retries: 0, ++ stage_id: "implement".to_string(), ++ stage_label: "implement".to_string(), ++ status: "success".to_string(), ++ duration_ms: 90_000, ++ retries: 0, + billing_usd_micros: Some(250_000), +- notes: None, +- failure_reason: None, +- files_touched: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()], ++ notes: None, ++ failure_reason: None, ++ files_touched: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()], + }, + StageRetro { +- stage_id: "simplify".to_string(), +- stage_label: "simplify".to_string(), +- status: "success".to_string(), +- duration_ms: 15_000, +- retries: 0, ++ stage_id: "simplify".to_string(), ++ stage_label: "simplify".to_string(), ++ status: "success".to_string(), ++ duration_ms: 15_000, ++ retries: 0, + billing_usd_micros: Some(50_000), +- notes: None, +- failure_reason: None, +- files_touched: vec![], ++ notes: None, ++ failure_reason: None, ++ files_touched: vec![], + }, + ], +- stats: AggregateStats { +- total_duration_ms: 150_000, ++ stats: AggregateStats { ++ total_duration_ms: 150_000, + total_billing_usd_micros: Some(420_000), +- total_retries: 0, +- files_touched: vec!["src/lib.rs".to_string(), "src/main.rs".to_string()], +- stages_completed: 3, +- stages_failed: 0, ++ total_retries: 0, ++ files_touched: vec!["src/lib.rs".to_string(), "src/main.rs".to_string()], ++ stages_completed: 3, ++ stages_failed: 0, + }, +- intent: None, +- outcome: None, +- learnings: None, ++ intent: None, ++ outcome: None, ++ learnings: None, + friction_points: Some(vec![ + FrictionPoint { +- kind: FrictionKind::ToolFailure, ++ kind: FrictionKind::ToolFailure, + description: "Daytona sandbox didn't have cargo on PATH".to_string(), +- stage_id: None, ++ stage_id: None, + }, + FrictionPoint { +- kind: FrictionKind::Timeout, ++ kind: FrictionKind::Timeout, + description: "Proxy timeouts during cold compilations".to_string(), +- stage_id: None, ++ stage_id: None, + }, + ]), +- open_items: Some(vec![OpenItem { +- kind: OpenItemKind::TechDebt, ++ open_items: Some(vec![OpenItem { ++ kind: OpenItemKind::TechDebt, + description: "`ToolApprovalFn` type alias still exists".to_string(), + }]), + } +@@ -835,25 +835,25 @@ mod tests { + #[test] + fn format_retro_section_empty_stats() { + let retro = Retro { +- run_id: fixtures::RUN_2, +- workflow_name: "test".to_string(), +- goal: "test".to_string(), +- timestamp: Utc::now(), +- smoothness: None, +- stages: vec![], +- stats: AggregateStats { +- total_duration_ms: 0, ++ run_id: fixtures::RUN_2, ++ workflow_name: "test".to_string(), ++ goal: "test".to_string(), ++ timestamp: Utc::now(), ++ smoothness: None, ++ stages: vec![], ++ stats: AggregateStats { ++ total_duration_ms: 0, + total_billing_usd_micros: None, +- total_retries: 0, +- files_touched: vec![], +- stages_completed: 0, +- stages_failed: 0, ++ total_retries: 0, ++ files_touched: vec![], ++ stages_completed: 0, ++ stages_failed: 0, + }, +- intent: None, +- outcome: None, +- learnings: None, ++ intent: None, ++ outcome: None, ++ learnings: None, + friction_points: None, +- open_items: None, ++ open_items: None, + }; + let section = format_retro_section(&retro); + +@@ -1084,43 +1084,51 @@ mod tests { + let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); + + let run_record = RunRecord { +- run_id: fixtures::RUN_1, +- settings: SettingsLayer::default(), +- graph: Graph::new("test"), +- workflow_slug: Some("test".to_string()), ++ run_id: fixtures::RUN_1, ++ settings: SettingsLayer::default(), ++ graph: Graph::new("test"), ++ workflow_slug: Some("test".to_string()), + working_directory: PathBuf::from("/tmp/project"), +- host_repo_path: Some("/tmp/project".to_string()), +- repo_origin_url: None, +- base_branch: Some("main".to_string()), +- labels: HashMap::new(), +- provenance: None, +- manifest_blob: None, +- definition_blob: None, ++ host_repo_path: Some("/tmp/project".to_string()), ++ repo_origin_url: None, ++ base_branch: Some("main".to_string()), ++ labels: HashMap::new(), ++ provenance: None, ++ manifest_blob: None, ++ definition_blob: None, + }; +- append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { +- run_id: fixtures::RUN_1, +- settings: serde_json::to_value(&run_record.settings).unwrap(), +- graph: serde_json::to_value(&run_record.graph).unwrap(), +- workflow_source: Some("digraph test { plan -> code }".to_string()), +- workflow_config: None, +- labels: run_record.labels.clone().into_iter().collect(), +- run_dir: run_record.working_directory.display().to_string(), +- working_directory: run_record.working_directory.display().to_string(), +- host_repo_path: run_record.host_repo_path.clone(), +- repo_origin_url: run_record.repo_origin_url.clone(), +- base_branch: run_record.base_branch.clone(), +- workflow_slug: run_record.workflow_slug.clone(), +- db_prefix: None, +- provenance: run_record.provenance.clone(), +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::RunCreated { ++ run_id: fixtures::RUN_1, ++ settings: serde_json::to_value(&run_record.settings).unwrap(), ++ graph: serde_json::to_value(&run_record.graph).unwrap(), ++ workflow_source: Some("digraph test { plan -> code }".to_string()), ++ workflow_config: None, ++ labels: run_record.labels.clone().into_iter().collect(), ++ run_dir: run_record.working_directory.display().to_string(), ++ working_directory: run_record.working_directory.display().to_string(), ++ host_repo_path: run_record.host_repo_path.clone(), ++ repo_origin_url: run_record.repo_origin_url.clone(), ++ base_branch: run_record.base_branch.clone(), ++ workflow_slug: run_record.workflow_slug.clone(), ++ db_prefix: None, ++ provenance: run_record.provenance.clone(), ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run_store, &fixtures::RUN_1, &Event::RetroCompleted { +- duration_ms: 1, +- response: Some(String::new()), +- retro: Some(serde_json::to_value(make_test_retro()).unwrap()), +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::RetroCompleted { ++ duration_ms: 1, ++ response: Some(String::new()), ++ retro: Some(serde_json::to_value(make_test_retro()).unwrap()), ++ }, ++ ) + .await + .unwrap(); + +@@ -1149,60 +1157,68 @@ mod tests { + let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); + + let run_record = RunRecord { +- run_id: fixtures::RUN_1, +- settings: SettingsLayer::default(), +- graph: Graph::new("test"), +- workflow_slug: Some("test".to_string()), ++ run_id: fixtures::RUN_1, ++ settings: SettingsLayer::default(), ++ graph: Graph::new("test"), ++ workflow_slug: Some("test".to_string()), + working_directory: PathBuf::from("/tmp/project"), +- host_repo_path: Some("/tmp/project".to_string()), +- repo_origin_url: None, +- base_branch: Some("main".to_string()), +- labels: HashMap::new(), +- provenance: None, +- manifest_blob: None, +- definition_blob: None, ++ host_repo_path: Some("/tmp/project".to_string()), ++ repo_origin_url: None, ++ base_branch: Some("main".to_string()), ++ labels: HashMap::new(), ++ provenance: None, ++ manifest_blob: None, ++ definition_blob: None, + }; +- append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { +- run_id: fixtures::RUN_1, +- settings: serde_json::to_value(&run_record.settings).unwrap(), +- graph: serde_json::to_value(&run_record.graph).unwrap(), +- workflow_source: Some("digraph test { plan -> code }".to_string()), +- workflow_config: None, +- labels: run_record.labels.clone().into_iter().collect(), +- run_dir: run_record.working_directory.display().to_string(), +- working_directory: run_record.working_directory.display().to_string(), +- host_repo_path: run_record.host_repo_path.clone(), +- repo_origin_url: run_record.repo_origin_url.clone(), +- base_branch: run_record.base_branch.clone(), +- workflow_slug: run_record.workflow_slug.clone(), +- db_prefix: None, +- provenance: run_record.provenance.clone(), +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::RunCreated { ++ run_id: fixtures::RUN_1, ++ settings: serde_json::to_value(&run_record.settings).unwrap(), ++ graph: serde_json::to_value(&run_record.graph).unwrap(), ++ workflow_source: Some("digraph test { plan -> code }".to_string()), ++ workflow_config: None, ++ labels: run_record.labels.clone().into_iter().collect(), ++ run_dir: run_record.working_directory.display().to_string(), ++ working_directory: run_record.working_directory.display().to_string(), ++ host_repo_path: run_record.host_repo_path.clone(), ++ repo_origin_url: run_record.repo_origin_url.clone(), ++ base_branch: run_record.base_branch.clone(), ++ workflow_slug: run_record.workflow_slug.clone(), ++ db_prefix: None, ++ provenance: run_record.provenance.clone(), ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run_store, &fixtures::RUN_1, &Event::StageCompleted { +- node_id: "plan".to_string(), +- name: "plan".to_string(), +- index: 0, +- duration_ms: 1, +- status: "success".to_string(), +- preferred_label: None, +- suggested_next_ids: vec![], +- billing: None, +- failure: None, +- notes: None, +- files_touched: vec![], +- context_updates: None, +- jump_to_node: None, +- context_values: None, +- node_visits: None, +- loop_failure_signatures: None, +- restart_failure_signatures: None, +- response: Some("Plan from store".to_string()), +- attempt: 1, +- max_attempts: 1, +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::StageCompleted { ++ node_id: "plan".to_string(), ++ name: "plan".to_string(), ++ index: 0, ++ duration_ms: 1, ++ status: "success".to_string(), ++ preferred_label: None, ++ suggested_next_ids: vec![], ++ billing: None, ++ failure: None, ++ notes: None, ++ files_touched: vec![], ++ context_updates: None, ++ jump_to_node: None, ++ context_values: None, ++ node_visits: None, ++ loop_failure_signatures: None, ++ restart_failure_signatures: None, ++ response: Some("Plan from store".to_string()), ++ attempt: 1, ++ max_attempts: 1, ++ }, ++ ) + .await + .unwrap(); + +@@ -1340,7 +1356,7 @@ mod tests { + let store = test_store(); + let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); + let creds = GitHubCredentials::App(fabro_github::GitHubAppCredentials { +- app_id: "123".to_string(), ++ app_id: "123".to_string(), + private_key_pem: "unused".to_string(), + }); + let result = maybe_open_pull_request( +@@ -1367,50 +1383,58 @@ mod tests { + let store = test_store(); + let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); + let run_record = RunRecord { +- run_id: fixtures::RUN_1, +- settings: SettingsLayer::default(), +- graph: Graph::new("test"), +- workflow_slug: None, ++ run_id: fixtures::RUN_1, ++ settings: SettingsLayer::default(), ++ graph: Graph::new("test"), ++ workflow_slug: None, + working_directory: tmp.path().to_path_buf(), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- labels: std::collections::HashMap::new(), +- provenance: None, +- manifest_blob: None, +- definition_blob: None, ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ labels: std::collections::HashMap::new(), ++ provenance: None, ++ manifest_blob: None, ++ definition_blob: None, + }; +- append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { +- run_id: fixtures::RUN_1, +- settings: serde_json::to_value(&run_record.settings).unwrap(), +- graph: serde_json::to_value(&run_record.graph).unwrap(), +- workflow_source: None, +- workflow_config: None, +- labels: run_record.labels.clone().into_iter().collect(), +- run_dir: run_record.working_directory.display().to_string(), +- working_directory: tmp.path().display().to_string(), +- host_repo_path: None, +- repo_origin_url: run_record.repo_origin_url.clone(), +- base_branch: None, +- workflow_slug: None, +- db_prefix: None, +- provenance: run_record.provenance.clone(), +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::RunCreated { ++ run_id: fixtures::RUN_1, ++ settings: serde_json::to_value(&run_record.settings).unwrap(), ++ graph: serde_json::to_value(&run_record.graph).unwrap(), ++ workflow_source: None, ++ workflow_config: None, ++ labels: run_record.labels.clone().into_iter().collect(), ++ run_dir: run_record.working_directory.display().to_string(), ++ working_directory: tmp.path().display().to_string(), ++ host_repo_path: None, ++ repo_origin_url: run_record.repo_origin_url.clone(), ++ base_branch: None, ++ workflow_slug: None, ++ db_prefix: None, ++ provenance: run_record.provenance.clone(), ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted { +- duration_ms: 1, +- artifact_count: 0, +- status: "success".to_string(), +- reason: None, +- total_usd_micros: None, +- final_git_commit_sha: None, +- final_patch: Some( +- "diff --git a/src/lib.rs b/src/lib.rs\n+fn from_store() {}\n".to_string(), +- ), +- billing: None, +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::WorkflowRunCompleted { ++ duration_ms: 1, ++ artifact_count: 0, ++ status: "success".to_string(), ++ reason: None, ++ total_usd_micros: None, ++ final_git_commit_sha: None, ++ final_patch: Some( ++ "diff --git a/src/lib.rs b/src/lib.rs\n+fn from_store() {}\n".to_string(), ++ ), ++ billing: None, ++ }, ++ ) + .await + .unwrap(); + +diff --git a/lib/crates/fabro-workflow/src/pipeline/retro.rs b/lib/crates/fabro-workflow/src/pipeline/retro.rs +index c61afe74..af93cc2f 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/retro.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/retro.rs +@@ -16,7 +16,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { + tracing::warn!(error = %e, "Could not load run state, skipping retro"); + if let Some(ref emitter) = options.emitter { + emitter.emit(&Event::RetroFailed { +- error: e.to_string(), ++ error: e.to_string(), + duration_ms: 0, + }); + } +@@ -27,7 +27,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { + tracing::warn!("Could not load checkpoint, skipping retro"); + if let Some(ref emitter) = options.emitter { + emitter.emit(&Event::RetroFailed { +- error: "checkpoint not found".to_string(), ++ error: "checkpoint not found".to_string(), + duration_ms: 0, + }); + } +@@ -55,9 +55,9 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { + let retro_prompt = build_retro_prompt(RETRO_DATA_DIR); + if let Some(ref emitter) = options.emitter { + emitter.emit(&Event::RetroStarted { +- prompt: Some(retro_prompt), ++ prompt: Some(retro_prompt), + provider: Some(options.provider.as_str().to_string()), +- model: Some(options.model.clone()), ++ model: Some(options.model.clone()), + }); + } + +@@ -71,10 +71,10 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { + emitter.touch(); + if !event.event.is_streaming_noise() { + emitter.emit(&Event::Agent { +- stage: "retro".to_string(), +- visit: 1, +- event: event.event.clone(), +- session_id: Some(event.session_id.clone()), ++ stage: "retro".to_string(), ++ visit: 1, ++ event: event.event.clone(), ++ session_id: Some(event.session_id.clone()), + parent_session_id: event.parent_session_id.clone(), + }); + } +@@ -86,7 +86,7 @@ pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option { + tracing::warn!(error = %err, "Could not load events from store, skipping retro"); + if let Some(ref emitter) = options.emitter { + emitter.emit(&Event::RetroFailed { +- error: err.to_string(), ++ error: err.to_string(), + duration_ms: 0, + }); + } +@@ -233,63 +233,71 @@ mod tests { + let inner = test_store().create_run(&test_run_id()).await.unwrap(); + let run_store = inner; + let run_record = RunRecord { +- run_id: test_run_id(), +- settings: SettingsLayer::default(), +- graph: Graph::new("test"), +- workflow_slug: None, ++ run_id: test_run_id(), ++ settings: SettingsLayer::default(), ++ graph: Graph::new("test"), ++ workflow_slug: None, + working_directory: run_dir.to_path_buf(), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- labels: std::collections::HashMap::new(), +- provenance: None, +- manifest_blob: None, +- definition_blob: None, ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ labels: std::collections::HashMap::new(), ++ provenance: None, ++ manifest_blob: None, ++ definition_blob: None, + }; +- append_event(&run_store, &test_run_id(), &Event::RunCreated { +- run_id: test_run_id(), +- settings: serde_json::to_value(&run_record.settings).unwrap(), +- graph: serde_json::to_value(&run_record.graph).unwrap(), +- workflow_source: None, +- workflow_config: None, +- labels: run_record.labels.clone().into_iter().collect(), +- run_dir: run_dir.to_string_lossy().to_string(), +- working_directory: run_dir.to_string_lossy().to_string(), +- host_repo_path: None, +- repo_origin_url: run_record.repo_origin_url.clone(), +- base_branch: None, +- workflow_slug: None, +- db_prefix: None, +- provenance: run_record.provenance.clone(), +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &test_run_id(), ++ &Event::RunCreated { ++ run_id: test_run_id(), ++ settings: serde_json::to_value(&run_record.settings).unwrap(), ++ graph: serde_json::to_value(&run_record.graph).unwrap(), ++ workflow_source: None, ++ workflow_config: None, ++ labels: run_record.labels.clone().into_iter().collect(), ++ run_dir: run_dir.to_string_lossy().to_string(), ++ working_directory: run_dir.to_string_lossy().to_string(), ++ host_repo_path: None, ++ repo_origin_url: run_record.repo_origin_url.clone(), ++ base_branch: None, ++ workflow_slug: None, ++ db_prefix: None, ++ provenance: run_record.provenance.clone(), ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run_store, &test_run_id(), &Event::CheckpointCompleted { +- node_id: checkpoint.current_node.clone(), +- status: "success".to_string(), +- current_node: checkpoint.current_node.clone(), +- completed_nodes: checkpoint.completed_nodes.clone(), +- node_retries: checkpoint.node_retries.clone().into_iter().collect(), +- context_values: checkpoint.context_values.clone().into_iter().collect(), +- node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), +- next_node_id: checkpoint.next_node_id.clone(), +- git_commit_sha: checkpoint.git_commit_sha.clone(), +- loop_failure_signatures: checkpoint +- .loop_failure_signatures +- .clone() +- .into_iter() +- .map(|(signature, count)| (signature.to_string(), count)) +- .collect(), +- restart_failure_signatures: checkpoint +- .restart_failure_signatures +- .clone() +- .into_iter() +- .map(|(signature, count)| (signature.to_string(), count)) +- .collect(), +- node_visits: checkpoint.node_visits.clone().into_iter().collect(), +- diff: None, +- }) ++ append_event( ++ &run_store, ++ &test_run_id(), ++ &Event::CheckpointCompleted { ++ node_id: checkpoint.current_node.clone(), ++ status: "success".to_string(), ++ current_node: checkpoint.current_node.clone(), ++ completed_nodes: checkpoint.completed_nodes.clone(), ++ node_retries: checkpoint.node_retries.clone().into_iter().collect(), ++ context_values: checkpoint.context_values.clone().into_iter().collect(), ++ node_outcomes: checkpoint.node_outcomes.clone().into_iter().collect(), ++ next_node_id: checkpoint.next_node_id.clone(), ++ git_commit_sha: checkpoint.git_commit_sha.clone(), ++ loop_failure_signatures: checkpoint ++ .loop_failure_signatures ++ .clone() ++ .into_iter() ++ .map(|(signature, count)| (signature.to_string(), count)) ++ .collect(), ++ restart_failure_signatures: checkpoint ++ .restart_failure_signatures ++ .clone() ++ .into_iter() ++ .map(|(signature, count)| (signature.to_string(), count)) ++ .collect(), ++ node_visits: checkpoint.node_visits.clone().into_iter().collect(), ++ diff: None, ++ }, ++ ) + .await + .unwrap(); + run_store +@@ -297,17 +305,17 @@ mod tests { + + fn test_run_options(run_dir: &std::path::Path) -> RunOptions { + RunOptions { +- settings: SettingsLayer::default(), +- run_dir: run_dir.to_path_buf(), +- cancel_token: None, +- run_id: test_run_id(), +- labels: HashMap::new(), +- workflow_slug: None, +- github_app: None, +- host_repo_path: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: run_dir.to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id(), ++ labels: HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ host_repo_path: None, ++ base_branch: None, + display_base_sha: None, +- git: None, ++ git: None, + } + } + +@@ -326,35 +334,38 @@ mod tests { + std::env::current_dir().unwrap(), + )); + let executed = Executed { +- graph: Graph::new("test"), +- outcome: Ok(crate::outcome::Outcome::success()), +- run_options: test_run_options(&run_dir), +- run_store: run_store.clone().into(), +- hook_runner: None, +- emitter: Arc::clone(&emitter), +- sandbox: Arc::clone(&sandbox), +- duration_ms: 1, ++ graph: Graph::new("test"), ++ outcome: Ok(crate::outcome::Outcome::success()), ++ run_options: test_run_options(&run_dir), ++ run_store: run_store.clone().into(), ++ hook_runner: None, ++ emitter: Arc::clone(&emitter), ++ sandbox: Arc::clone(&sandbox), ++ duration_ms: 1, + final_context: Context::new(), +- llm_client: None, +- model: "test-model".to_string(), +- provider: fabro_llm::Provider::Anthropic, +- }; +- +- let retroed = retro(executed, &RetroOptions { +- run_id: test_run_id(), +- run_store: run_store.into(), +- workflow_name: "test".to_string(), +- goal: "Ship it".to_string(), +- run_dir: run_dir.clone(), +- sandbox, +- emitter: Some(emitter), +- failed: false, +- run_duration_ms: 1, +- enabled: true, + llm_client: None, +- provider: fabro_llm::Provider::Anthropic, + model: "test-model".to_string(), +- }) ++ provider: fabro_llm::Provider::Anthropic, ++ }; ++ ++ let retroed = retro( ++ executed, ++ &RetroOptions { ++ run_id: test_run_id(), ++ run_store: run_store.into(), ++ workflow_name: "test".to_string(), ++ goal: "Ship it".to_string(), ++ run_dir: run_dir.clone(), ++ sandbox, ++ emitter: Some(emitter), ++ failed: false, ++ run_duration_ms: 1, ++ enabled: true, ++ llm_client: None, ++ provider: fabro_llm::Provider::Anthropic, ++ model: "test-model".to_string(), ++ }, ++ ) + .await; + store_logger.flush().await; + +@@ -377,21 +388,21 @@ mod tests { + + let retro = run_retro( + &RetroOptions { +- run_id: test_run_id(), +- run_store: test_run_store(&run_dir, &checkpoint).await.into(), +- workflow_name: "test".to_string(), +- goal: "Ship it".to_string(), +- run_dir: run_dir.clone(), +- sandbox: Arc::new(fabro_agent::LocalSandbox::new( ++ run_id: test_run_id(), ++ run_store: test_run_store(&run_dir, &checkpoint).await.into(), ++ workflow_name: "test".to_string(), ++ goal: "Ship it".to_string(), ++ run_dir: run_dir.clone(), ++ sandbox: Arc::new(fabro_agent::LocalSandbox::new( + std::env::current_dir().unwrap(), + )), +- emitter: Some(Arc::clone(&emitter)), +- failed: false, ++ emitter: Some(Arc::clone(&emitter)), ++ failed: false, + run_duration_ms: 1, +- enabled: true, +- llm_client: None, +- provider: fabro_llm::Provider::Anthropic, +- model: "test-model".to_string(), ++ enabled: true, ++ llm_client: None, ++ provider: fabro_llm::Provider::Anthropic, ++ model: "test-model".to_string(), + }, + true, + ) +diff --git a/lib/crates/fabro-workflow/src/pipeline/transform.rs b/lib/crates/fabro-workflow/src/pipeline/transform.rs +index ced3b257..308f4fff 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/transform.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/transform.rs +@@ -81,12 +81,15 @@ mod tests { + start -> work -> exit + }"#; + let parsed = parse(dot).unwrap(); +- let transformed = transform(parsed, &TransformOptions { +- current_dir: None, +- file_resolver: None, +- inputs: HashMap::new(), +- custom_transforms: vec![], +- }) ++ let transformed = transform( ++ parsed, ++ &TransformOptions { ++ current_dir: None, ++ file_resolver: None, ++ inputs: HashMap::new(), ++ custom_transforms: vec![], ++ }, ++ ) + .unwrap(); + let prompt = transformed.graph.nodes["work"] + .attrs +@@ -106,12 +109,15 @@ mod tests { + start -> work -> exit + }"#; + let parsed = parse(dot).unwrap(); +- let transformed = transform(parsed, &TransformOptions { +- current_dir: None, +- file_resolver: None, +- inputs: HashMap::new(), +- custom_transforms: vec![], +- }) ++ let transformed = transform( ++ parsed, ++ &TransformOptions { ++ current_dir: None, ++ file_resolver: None, ++ inputs: HashMap::new(), ++ custom_transforms: vec![], ++ }, ++ ) + .unwrap(); + assert_eq!( + transformed.graph.nodes["work"].attrs.get("model"), +@@ -134,12 +140,15 @@ mod tests { + }"#, + ) + .unwrap(); +- let transformed = transform(parsed, &TransformOptions { +- current_dir: Some(dir.path().to_path_buf()), +- file_resolver: Some(Arc::new(FilesystemFileResolver::new(None))), +- inputs: HashMap::new(), +- custom_transforms: vec![], +- }) ++ let transformed = transform( ++ parsed, ++ &TransformOptions { ++ current_dir: Some(dir.path().to_path_buf()), ++ file_resolver: Some(Arc::new(FilesystemFileResolver::new(None))), ++ inputs: HashMap::new(), ++ custom_transforms: vec![], ++ }, ++ ) + .unwrap(); + + assert_eq!( +@@ -178,15 +187,18 @@ mod tests { + }"#, + ) + .unwrap(); +- let transformed = transform(parsed, &TransformOptions { +- current_dir: Some(dir.path().to_path_buf()), +- file_resolver: Some(Arc::new(FilesystemFileResolver::new(None))), +- inputs: HashMap::from([( +- "task".to_string(), +- toml::Value::String("Launch".to_string()), +- )]), +- custom_transforms: vec![], +- }) ++ let transformed = transform( ++ parsed, ++ &TransformOptions { ++ current_dir: Some(dir.path().to_path_buf()), ++ file_resolver: Some(Arc::new(FilesystemFileResolver::new(None))), ++ inputs: HashMap::from([( ++ "task".to_string(), ++ toml::Value::String("Launch".to_string()), ++ )]), ++ custom_transforms: vec![], ++ }, ++ ) + .unwrap(); + + let lint = &transformed.graph.nodes["validate.lint"]; +diff --git a/lib/crates/fabro-workflow/src/pipeline/types.rs b/lib/crates/fabro-workflow/src/pipeline/types.rs +index 1c69b3fe..2e3be971 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/types.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/types.rs +@@ -36,7 +36,7 @@ use crate::workflow_bundle::WorkflowBundle; + /// Output of the PARSE phase. + #[non_exhaustive] + pub struct Parsed { +- pub graph: Graph, ++ pub graph: Graph, + pub source: String, + } + +@@ -44,7 +44,7 @@ pub struct Parsed { + /// post-transform adjustments (e.g. goal override) before validation. + #[non_exhaustive] + pub struct Transformed { +- pub graph: Graph, ++ pub graph: Graph, + pub source: String, + } + +@@ -53,8 +53,8 @@ pub struct Transformed { + /// Graph is read-only — use accessors, not direct field access. + #[non_exhaustive] + pub struct Validated { +- graph: Graph, +- source: String, ++ graph: Graph, ++ source: String, + diagnostics: Vec, + } + +@@ -113,7 +113,7 @@ impl Validated { + + /// Options for the PERSIST phase. + pub(crate) struct PersistOptions { +- pub run_dir: PathBuf, ++ pub run_dir: PathBuf, + pub run_record: RunRecord, + } + +@@ -122,11 +122,11 @@ pub(crate) struct PersistOptions { + #[derive(Debug)] + #[non_exhaustive] + pub struct Persisted { +- graph: Graph, +- source: String, ++ graph: Graph, ++ source: String, + diagnostics: Vec, +- run_dir: PathBuf, +- run_record: RunRecord, ++ run_dir: PathBuf, ++ run_record: RunRecord, + } + + impl Persisted { +@@ -212,173 +212,173 @@ impl Persisted { + + #[derive(Clone)] + pub struct LlmSpec { +- pub model: String, +- pub provider: Provider, ++ pub model: String, ++ pub provider: Provider, + pub fallback_chain: Vec, +- pub mcp_servers: Vec, +- pub dry_run: bool, ++ pub mcp_servers: Vec, ++ pub dry_run: bool, + } + + #[derive(Clone)] + pub struct SandboxEnvSpec { +- pub devcontainer_env: HashMap, +- pub toml_env: HashMap, ++ pub devcontainer_env: HashMap, ++ pub toml_env: HashMap, + pub github_permissions: Option>, +- pub origin_url: Option, ++ pub origin_url: Option, + } + + #[derive(Clone)] + pub struct DevcontainerSpec { +- pub enabled: bool, ++ pub enabled: bool, + pub resolve_dir: PathBuf, + } + + pub struct InitOptions { +- pub run_id: RunId, +- pub run_store: RunStoreHandle, +- pub dry_run: bool, +- pub emitter: Arc, +- pub sandbox: SandboxSpec, +- pub llm: LlmSpec, +- pub interviewer: Arc, +- pub lifecycle: LifecycleOptions, +- pub run_options: RunOptions, +- pub workflow_path: Option, +- pub workflow_bundle: Option>, +- pub hooks: fabro_hooks::HookSettings, +- pub sandbox_env: SandboxEnvSpec, +- pub vault: Option>>, +- pub devcontainer: Option, +- pub git: Option, +- pub worktree_mode: Option, ++ pub run_id: RunId, ++ pub run_store: RunStoreHandle, ++ pub dry_run: bool, ++ pub emitter: Arc, ++ pub sandbox: SandboxSpec, ++ pub llm: LlmSpec, ++ pub interviewer: Arc, ++ pub lifecycle: LifecycleOptions, ++ pub run_options: RunOptions, ++ pub workflow_path: Option, ++ pub workflow_bundle: Option>, ++ pub hooks: fabro_hooks::HookSettings, ++ pub sandbox_env: SandboxEnvSpec, ++ pub vault: Option>>, ++ pub devcontainer: Option, ++ pub git: Option, ++ pub worktree_mode: Option, + pub registry_override: Option>, +- pub artifact_sink: Option, +- pub run_control: Option>, +- pub checkpoint: Option, +- pub seed_context: Option, ++ pub artifact_sink: Option, ++ pub run_control: Option>, ++ pub checkpoint: Option, ++ pub seed_context: Option, + } + + /// Output of the INITIALIZE phase. + #[non_exhaustive] + pub struct Initialized { +- pub graph: Graph, +- pub source: String, +- pub inputs: HashMap, +- pub run_options: RunOptions, +- pub workflow_path: Option, +- pub workflow_bundle: Option>, +- pub run_store: RunStoreHandle, +- pub(crate) checkpoint: Option, ++ pub graph: Graph, ++ pub source: String, ++ pub inputs: HashMap, ++ pub run_options: RunOptions, ++ pub workflow_path: Option, ++ pub workflow_bundle: Option>, ++ pub run_store: RunStoreHandle, ++ pub(crate) checkpoint: Option, + pub(crate) seed_context: Option, +- pub emitter: Arc, +- pub sandbox: Arc, +- pub registry: Arc, +- pub on_node: crate::OnNodeCallback, +- pub artifact_sink: Option, +- pub run_control: Option>, +- pub hook_runner: Option>, +- pub env: HashMap, +- pub dry_run: bool, +- pub llm_client: Option, +- pub model: String, +- pub provider: Provider, ++ pub emitter: Arc, ++ pub sandbox: Arc, ++ pub registry: Arc, ++ pub on_node: crate::OnNodeCallback, ++ pub artifact_sink: Option, ++ pub run_control: Option>, ++ pub hook_runner: Option>, ++ pub env: HashMap, ++ pub dry_run: bool, ++ pub llm_client: Option, ++ pub model: String, ++ pub provider: Provider, + } + + /// Output of the EXECUTE phase. + #[non_exhaustive] + pub struct Executed { +- pub graph: Graph, +- pub outcome: Result, +- pub run_options: RunOptions, +- pub run_store: RunStoreHandle, +- pub hook_runner: Option>, +- pub emitter: Arc, +- pub sandbox: Arc, +- pub duration_ms: u64, ++ pub graph: Graph, ++ pub outcome: Result, ++ pub run_options: RunOptions, ++ pub run_store: RunStoreHandle, ++ pub hook_runner: Option>, ++ pub emitter: Arc, ++ pub sandbox: Arc, ++ pub duration_ms: u64, + pub final_context: Context, +- pub llm_client: Option, +- pub model: String, +- pub provider: Provider, ++ pub llm_client: Option, ++ pub model: String, ++ pub provider: Provider, + } + + /// Output of the RETRO phase. + #[non_exhaustive] + pub struct Retroed { +- pub graph: Graph, +- pub outcome: Result, ++ pub graph: Graph, ++ pub outcome: Result, + pub run_options: RunOptions, +- pub run_store: RunStoreHandle, ++ pub run_store: RunStoreHandle, + pub hook_runner: Option>, +- pub emitter: Arc, +- pub sandbox: Arc, ++ pub emitter: Arc, ++ pub sandbox: Arc, + pub duration_ms: u64, +- pub retro: Option, ++ pub retro: Option, + } + + /// Output of the FINALIZE phase. + #[non_exhaustive] + pub struct Concluded { +- pub run_id: RunId, +- pub outcome: Result, +- pub conclusion: Conclusion, ++ pub run_id: RunId, ++ pub outcome: Result, ++ pub conclusion: Conclusion, + pub pushed_branch: Option, +- pub graph: Graph, +- pub run_options: RunOptions, +- pub emitter: Arc, ++ pub graph: Graph, ++ pub run_options: RunOptions, ++ pub emitter: Arc, + } + + /// Output of the PULL_REQUEST phase. + #[non_exhaustive] + pub struct Finalized { +- pub run_id: RunId, +- pub outcome: Result, +- pub conclusion: Conclusion, ++ pub run_id: RunId, ++ pub outcome: Result, ++ pub conclusion: Conclusion, + pub pushed_branch: Option, +- pub pr_url: Option, ++ pub pr_url: Option, + } + + /// Options for the TRANSFORM phase. + pub struct TransformOptions { +- pub current_dir: Option, +- pub file_resolver: Option>, +- pub inputs: HashMap, ++ pub current_dir: Option, ++ pub file_resolver: Option>, ++ pub inputs: HashMap, + pub custom_transforms: Vec>, + } + + /// Options for the RETRO phase. + pub struct RetroOptions { +- pub run_id: RunId, +- pub run_store: RunStoreHandle, +- pub workflow_name: String, +- pub goal: String, +- pub run_dir: PathBuf, +- pub sandbox: Arc, +- pub emitter: Option>, +- pub failed: bool, ++ pub run_id: RunId, ++ pub run_store: RunStoreHandle, ++ pub workflow_name: String, ++ pub goal: String, ++ pub run_dir: PathBuf, ++ pub sandbox: Arc, ++ pub emitter: Option>, ++ pub failed: bool, + pub run_duration_ms: u64, +- pub enabled: bool, +- pub llm_client: Option, +- pub provider: Provider, +- pub model: String, ++ pub enabled: bool, ++ pub llm_client: Option, ++ pub provider: Provider, ++ pub model: String, + } + + /// Options for the FINALIZE phase. + pub struct FinalizeOptions { +- pub run_dir: PathBuf, +- pub run_id: RunId, +- pub run_store: RunStoreHandle, +- pub workflow_name: String, +- pub hook_runner: Option>, ++ pub run_dir: PathBuf, ++ pub run_id: RunId, ++ pub run_store: RunStoreHandle, ++ pub workflow_name: String, ++ pub hook_runner: Option>, + pub preserve_sandbox: bool, +- pub last_git_sha: Option, ++ pub last_git_sha: Option, + } + + /// Options for the PULL_REQUEST phase. + pub struct PullRequestOptions { +- pub run_dir: PathBuf, +- pub run_store: RunStoreHandle, +- pub pr_config: Option, ++ pub run_dir: PathBuf, ++ pub run_store: RunStoreHandle, ++ pub pr_config: Option, + pub github_app: Option, + pub origin_url: Option, +- pub model: String, ++ pub model: String, + } +diff --git a/lib/crates/fabro-workflow/src/pipeline/validate.rs b/lib/crates/fabro-workflow/src/pipeline/validate.rs +index 83c140ac..e07cf874 100644 +--- a/lib/crates/fabro-workflow/src/pipeline/validate.rs ++++ b/lib/crates/fabro-workflow/src/pipeline/validate.rs +@@ -21,12 +21,15 @@ mod tests { + + fn run_pipeline(dot: &str) -> Validated { + let parsed = parse(dot).unwrap(); +- let transformed = transform::transform(parsed, &TransformOptions { +- current_dir: None, +- file_resolver: None, +- inputs: std::collections::HashMap::new(), +- custom_transforms: vec![], +- }) ++ let transformed = transform::transform( ++ parsed, ++ &TransformOptions { ++ current_dir: None, ++ file_resolver: None, ++ inputs: std::collections::HashMap::new(), ++ custom_transforms: vec![], ++ }, ++ ) + .unwrap(); + validate(transformed, &[]) + } +diff --git a/lib/crates/fabro-workflow/src/retry.rs b/lib/crates/fabro-workflow/src/retry.rs +index 3a92925b..72b12241 100644 +--- a/lib/crates/fabro-workflow/src/retry.rs ++++ b/lib/crates/fabro-workflow/src/retry.rs +@@ -5,9 +5,9 @@ use fabro_graphviz::graph::types::{Graph as GvGraph, Node as GvNode}; + + const DEFAULT_BACKOFF: BackoffPolicy = BackoffPolicy { + initial_delay: Duration::from_millis(5_000), +- factor: 2.0, +- max_delay: Duration::from_millis(60_000), +- jitter: true, ++ factor: 2.0, ++ max_delay: Duration::from_millis(60_000), ++ jitter: true, + }; + + /// Build a retry policy from node and graph attributes. +@@ -35,22 +35,22 @@ fn preset_retry_policy(preset: &str) -> Option { + match preset { + "none" => Some(RetryPolicy { + max_attempts: 1, +- backoff: DEFAULT_BACKOFF, ++ backoff: DEFAULT_BACKOFF, + }), + "standard" => Some(RetryPolicy { + max_attempts: 5, +- backoff: DEFAULT_BACKOFF, ++ backoff: DEFAULT_BACKOFF, + }), + "aggressive" => Some(RetryPolicy { + max_attempts: 5, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(500), + ..DEFAULT_BACKOFF + }, + }), + "linear" => Some(RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(500), + factor: 1.0, + ..DEFAULT_BACKOFF +@@ -58,7 +58,7 @@ fn preset_retry_policy(preset: &str) -> Option { + }), + "patient" => Some(RetryPolicy { + max_attempts: 3, +- backoff: BackoffPolicy { ++ backoff: BackoffPolicy { + initial_delay: Duration::from_millis(2_000), + factor: 3.0, + ..DEFAULT_BACKOFF +diff --git a/lib/crates/fabro-workflow/src/run_control.rs b/lib/crates/fabro-workflow/src/run_control.rs +index 6a61f105..3bb4ca0a 100644 +--- a/lib/crates/fabro-workflow/src/run_control.rs ++++ b/lib/crates/fabro-workflow/src/run_control.rs +@@ -8,7 +8,7 @@ use crate::event::{Emitter, Event}; + #[derive(Default)] + pub struct RunControlState { + pause_requested: AtomicBool, +- notify: Notify, ++ notify: Notify, + } + + impl RunControlState { +diff --git a/lib/crates/fabro-workflow/src/run_dump.rs b/lib/crates/fabro-workflow/src/run_dump.rs +index 509c3b09..ec8f573c 100644 +--- a/lib/crates/fabro-workflow/src/run_dump.rs ++++ b/lib/crates/fabro-workflow/src/run_dump.rs +@@ -17,7 +17,7 @@ pub struct RunDump { + + #[derive(Debug, Clone)] + pub struct RunDumpEntry { +- path: String, ++ path: String, + contents: RunDumpContents, + } + +@@ -298,42 +298,42 @@ impl RunDump { + impl RunDumpEntry { + fn text(path: impl Into, contents: String) -> Self { + Self { +- path: path.into(), ++ path: path.into(), + contents: RunDumpContents::Text(contents), + } + } + + fn text_path(path: &Path, contents: String) -> Self { + Self { +- path: path_to_string(path), ++ path: path_to_string(path), + contents: RunDumpContents::Text(contents), + } + } + + fn json(path: impl Into, contents: serde_json::Value) -> Self { + Self { +- path: path.into(), ++ path: path.into(), + contents: RunDumpContents::Json(contents), + } + } + + fn json_path(path: &Path, contents: serde_json::Value) -> Self { + Self { +- path: path_to_string(path), ++ path: path_to_string(path), + contents: RunDumpContents::Json(contents), + } + } + + fn bytes(path: impl Into, contents: Vec) -> Self { + Self { +- path: path.into(), ++ path: path.into(), + contents: RunDumpContents::Bytes(contents), + } + } + + fn bytes_path(path: &Path, contents: Vec) -> Self { + Self { +- path: path_to_string(path), ++ path: path_to_string(path), + contents: RunDumpContents::Bytes(contents), + } + } +diff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs +index 24036067..95e301aa 100644 +--- a/lib/crates/fabro-workflow/src/run_lookup.rs ++++ b/lib/crates/fabro-workflow/src/run_lookup.rs +@@ -13,26 +13,26 @@ use crate::run_status::{RunStatus, StatusReason}; + + #[derive(Debug, Clone)] + struct RunLocalState { +- dir_name: String, ++ dir_name: String, + start_time_dt: Option>, +- end_time: Option>, +- path: PathBuf, +- is_orphan: bool, ++ end_time: Option>, ++ path: PathBuf, ++ is_orphan: bool, + } + + #[derive(Debug, Clone, Serialize)] + pub struct RunInfo { + #[serde(skip)] +- summary: Option, +- pub dir_name: String, ++ summary: Option, ++ pub dir_name: String, + #[serde(skip)] + pub start_time_dt: Option>, + #[serde(skip)] +- pub end_time: Option>, ++ pub end_time: Option>, + #[serde(skip)] +- pub path: PathBuf, ++ pub path: PathBuf, + #[serde(skip)] +- pub is_orphan: bool, ++ pub is_orphan: bool, + } + + impl RunInfo { +@@ -170,13 +170,16 @@ fn scan_orphan_runs(base: &Path) -> Result> { + .and_then(|m| m.modified().ok()) + .map(|time| -> DateTime { time.into() }); + +- runs.push(RunInfo::new(None, RunLocalState { +- dir_name, +- start_time_dt: mtime_dt, +- end_time: None, +- path, +- is_orphan: true, +- })); ++ runs.push(RunInfo::new( ++ None, ++ RunLocalState { ++ dir_name, ++ start_time_dt: mtime_dt, ++ end_time: None, ++ path, ++ is_orphan: true, ++ }, ++ )); + } + + runs.sort_by(|a, b| { +@@ -240,13 +243,16 @@ fn run_info_from_summary(summary: &RunSummary, scratch_base: &Path) -> Option RunRecord { + RunRecord { +- run_id: fixtures::RUN_1, +- settings: SettingsLayer::default(), +- graph: Graph::new("test"), +- workflow_slug: Some("test".to_string()), ++ run_id: fixtures::RUN_1, ++ settings: SettingsLayer::default(), ++ graph: Graph::new("test"), ++ workflow_slug: Some("test".to_string()), + working_directory: PathBuf::from("/tmp/project"), +- host_repo_path: Some("/tmp/project".to_string()), +- repo_origin_url: None, +- base_branch: Some("main".to_string()), +- labels: HashMap::new(), +- provenance: None, +- manifest_blob: None, +- definition_blob: None, ++ host_repo_path: Some("/tmp/project".to_string()), ++ repo_origin_url: None, ++ base_branch: Some("main".to_string()), ++ labels: HashMap::new(), ++ provenance: None, ++ manifest_blob: None, ++ definition_blob: None, + } + } + +@@ -435,29 +441,37 @@ mod tests { + let store = memory_store(); + let run_record = sample_run_record(); + let run_store = store.create_run(&fixtures::RUN_1).await.unwrap(); +- append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { +- run_id: fixtures::RUN_1, +- settings: serde_json::to_value(&run_record.settings).unwrap(), +- graph: serde_json::to_value(&run_record.graph).unwrap(), +- workflow_source: None, +- workflow_config: None, +- labels: run_record.labels.clone().into_iter().collect(), +- run_dir: run_dir.display().to_string(), +- working_directory: run_record.working_directory.display().to_string(), +- host_repo_path: run_record.host_repo_path.clone(), +- repo_origin_url: run_record.repo_origin_url.clone(), +- base_branch: run_record.base_branch.clone(), +- workflow_slug: run_record.workflow_slug.clone(), +- db_prefix: None, +- provenance: run_record.provenance.clone(), +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::RunCreated { ++ run_id: fixtures::RUN_1, ++ settings: serde_json::to_value(&run_record.settings).unwrap(), ++ graph: serde_json::to_value(&run_record.graph).unwrap(), ++ workflow_source: None, ++ workflow_config: None, ++ labels: run_record.labels.clone().into_iter().collect(), ++ run_dir: run_dir.display().to_string(), ++ working_directory: run_record.working_directory.display().to_string(), ++ host_repo_path: run_record.host_repo_path.clone(), ++ repo_origin_url: run_record.repo_origin_url.clone(), ++ base_branch: run_record.base_branch.clone(), ++ workflow_slug: run_record.workflow_slug.clone(), ++ db_prefix: None, ++ provenance: run_record.provenance.clone(), ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); +- append_event(&run_store, &fixtures::RUN_1, &Event::RunSubmitted { +- reason: None, +- definition_blob: None, +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::RunSubmitted { ++ reason: None, ++ definition_blob: None, ++ }, ++ ) + .await + .unwrap(); + +diff --git a/lib/crates/fabro-workflow/src/run_options.rs b/lib/crates/fabro-workflow/src/run_options.rs +index 66beb351..77e944ce 100644 +--- a/lib/crates/fabro-workflow/src/run_options.rs ++++ b/lib/crates/fabro-workflow/src/run_options.rs +@@ -12,34 +12,34 @@ use crate::git::{GitAuthor, git_author_from_settings}; + /// Git checkpoint options for a workflow run. + #[derive(Clone)] + pub struct GitCheckpointOptions { +- pub base_sha: Option, +- pub run_branch: Option, ++ pub base_sha: Option, ++ pub run_branch: Option, + pub meta_branch: Option, + } + + /// Options for a workflow run. + #[derive(Clone)] + pub struct RunOptions { +- pub settings: SettingsLayer, +- pub run_dir: PathBuf, +- pub cancel_token: Option>, ++ pub settings: SettingsLayer, ++ pub run_dir: PathBuf, ++ pub cancel_token: Option>, + /// Unique identifier for this workflow run. +- pub run_id: RunId, ++ pub run_id: RunId, + /// User-defined key-value labels for this run. +- pub labels: HashMap, ++ pub labels: HashMap, + /// Workflow directory slug (e.g. "smoke" from `.fabro/workflows/smoke/`). +- pub workflow_slug: Option, ++ pub workflow_slug: Option, + /// GitHub credentials for pushing metadata branches to origin. +- pub github_app: Option, ++ pub github_app: Option, + /// Host repo path for MetadataStore (shadow commits) and host-side pushes. +- pub host_repo_path: Option, ++ pub host_repo_path: Option, + /// Name of the branch the run was started from (for PR base). +- pub base_branch: Option, ++ pub base_branch: Option, + /// Base commit SHA to display in lifecycle events/UI even when + /// checkpointing is disabled. + pub display_base_sha: Option, + /// Git checkpoint options; `None` means checkpointing disabled. +- pub git: Option, ++ pub git: Option, + } + + impl RunOptions { +@@ -69,9 +69,9 @@ impl RunOptions { + /// Options for sandbox lifecycle management within the engine. + pub struct LifecycleOptions { + /// Setup commands to run inside the sandbox after initialization. +- pub setup_commands: Vec, ++ pub setup_commands: Vec, + /// Timeout in milliseconds for each setup command. + pub setup_command_timeout_ms: u64, + /// Devcontainer lifecycle phases and their commands. +- pub devcontainer_phases: Vec<(String, Vec)>, ++ pub devcontainer_phases: Vec<(String, Vec)>, + } +diff --git a/lib/crates/fabro-workflow/src/runtime_store.rs b/lib/crates/fabro-workflow/src/runtime_store.rs +index d2523be1..90e5b231 100644 +--- a/lib/crates/fabro-workflow/src/runtime_store.rs ++++ b/lib/crates/fabro-workflow/src/runtime_store.rs +@@ -132,18 +132,18 @@ mod tests { + + fn test_run_record() -> RunRecord { + RunRecord { +- run_id: fixtures::RUN_1, +- settings: SettingsLayer::default(), +- graph: Graph::new("test"), +- workflow_slug: Some("test".to_string()), ++ run_id: fixtures::RUN_1, ++ settings: SettingsLayer::default(), ++ graph: Graph::new("test"), ++ workflow_slug: Some("test".to_string()), + working_directory: PathBuf::from("/tmp/test"), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- labels: HashMap::new(), +- provenance: None, +- manifest_blob: None, +- definition_blob: None, ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ labels: HashMap::new(), ++ provenance: None, ++ manifest_blob: None, ++ definition_blob: None, + } + } + +@@ -151,23 +151,27 @@ mod tests { + async fn local_handle_loads_state_and_events() { + let run_store = test_run_store().await; + let record = test_run_record(); +- append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { +- run_id: fixtures::RUN_1, +- settings: serde_json::to_value(&record.settings).unwrap(), +- graph: serde_json::to_value(&record.graph).unwrap(), +- workflow_source: Some("digraph test {}".to_string()), +- workflow_config: None, +- labels: std::collections::BTreeMap::new(), +- run_dir: "/tmp/test".to_string(), +- working_directory: "/tmp/test".to_string(), +- host_repo_path: None, +- repo_origin_url: None, +- base_branch: None, +- workflow_slug: Some("test".to_string()), +- db_prefix: None, +- provenance: None, +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &fixtures::RUN_1, ++ &Event::RunCreated { ++ run_id: fixtures::RUN_1, ++ settings: serde_json::to_value(&record.settings).unwrap(), ++ graph: serde_json::to_value(&record.graph).unwrap(), ++ workflow_source: Some("digraph test {}".to_string()), ++ workflow_config: None, ++ labels: std::collections::BTreeMap::new(), ++ run_dir: "/tmp/test".to_string(), ++ working_directory: "/tmp/test".to_string(), ++ host_repo_path: None, ++ repo_origin_url: None, ++ base_branch: None, ++ workflow_slug: Some("test".to_string()), ++ db_prefix: None, ++ provenance: None, ++ manifest_blob: None, ++ }, ++ ) + .await + .unwrap(); + +@@ -185,20 +189,20 @@ mod tests { + let handle = RunStoreHandle::local(run_store); + + let event = RunEvent { +- id: "evt-run-submitted".to_string(), +- ts: Utc::now(), +- run_id: fixtures::RUN_1, +- node_id: None, +- node_label: None, +- stage_id: None, +- parallel_group_id: None, ++ id: "evt-run-submitted".to_string(), ++ ts: Utc::now(), ++ run_id: fixtures::RUN_1, ++ node_id: None, ++ node_label: None, ++ stage_id: None, ++ parallel_group_id: None, + parallel_branch_id: None, +- session_id: None, +- parent_session_id: None, +- tool_call_id: None, +- actor: None, +- body: EventBody::RunSubmitted(RunSubmittedProps { +- reason: None, ++ session_id: None, ++ parent_session_id: None, ++ tool_call_id: None, ++ actor: None, ++ body: EventBody::RunSubmitted(RunSubmittedProps { ++ reason: None, + definition_blob: None, + }), + }; +diff --git a/lib/crates/fabro-workflow/src/sandbox_git.rs b/lib/crates/fabro-workflow/src/sandbox_git.rs +index 75fff9d4..1b67f2b0 100644 +--- a/lib/crates/fabro-workflow/src/sandbox_git.rs ++++ b/lib/crates/fabro-workflow/src/sandbox_git.rs +@@ -12,12 +12,12 @@ use crate::git::{GitAuthor, blocking_push_with_timeout, push_ref}; + /// Captured git state for a workflow run, shared with handlers. + #[derive(Debug, Clone)] + pub struct GitState { +- pub run_id: RunId, +- pub base_sha: String, +- pub run_branch: Option, +- pub meta_branch: Option, ++ pub run_id: RunId, ++ pub base_sha: String, ++ pub run_branch: Option, ++ pub meta_branch: Option, + pub checkpoint_exclude_globs: Vec, +- pub git_author: GitAuthor, ++ pub git_author: GitAuthor, + } + + pub const GIT_REMOTE: &str = "git -c maintenance.auto=0 -c gc.auto=0"; +@@ -71,18 +71,18 @@ pub async fn git_checkpoint( + let completed_str = completed_count.to_string(); + let mut trailers = vec![ + Trailer { +- key: "Fabro-Run", ++ key: "Fabro-Run", + value: run_id, + }, + Trailer { +- key: "Fabro-Completed", ++ key: "Fabro-Completed", + value: &completed_str, + }, + ]; + let shadow_sha_ref = shadow_sha.as_deref().unwrap_or(""); + if shadow_sha.is_some() { + trailers.push(Trailer { +- key: "Fabro-Checkpoint", ++ key: "Fabro-Checkpoint", + value: shadow_sha_ref, + }); + } +diff --git a/lib/crates/fabro-workflow/src/test_support.rs b/lib/crates/fabro-workflow/src/test_support.rs +index d2d51b0a..cd6e684c 100644 +--- a/lib/crates/fabro-workflow/src/test_support.rs ++++ b/lib/crates/fabro-workflow/src/test_support.rs +@@ -30,12 +30,12 @@ pub fn test_store_dir(run_dir: &std::path::Path) -> PathBuf { + + struct InitializedOptions { + hook_runner: Option>, +- env: HashMap, +- checkpoint: Option, ++ env: HashMap, ++ checkpoint: Option, + } + + struct InitializedState { +- initialized: Initialized, ++ initialized: Initialized, + store_logger: StoreProgressLogger, + } + +@@ -72,33 +72,37 @@ async fn initialized( + .await + .expect("failed to create slate-backed test run store"); + let run_store = inner_store; +- append_event(&run_store, &run_options.run_id, &Event::RunCreated { +- run_id: run_options.run_id, +- settings: serde_json::to_value(&run_options.settings) +- .expect("failed to serialize settings"), +- graph: serde_json::to_value(graph).expect("failed to serialize graph"), +- workflow_source: None, +- workflow_config: None, +- labels: run_options +- .labels +- .clone() +- .into_iter() +- .collect::>(), +- run_dir: run_options.run_dir.display().to_string(), +- working_directory: PathBuf::from(sandbox.working_directory()) +- .display() +- .to_string(), +- host_repo_path: run_options +- .host_repo_path +- .as_ref() +- .map(|path| path.display().to_string()), +- repo_origin_url: None, +- base_branch: run_options.base_branch.clone(), +- workflow_slug: run_options.workflow_slug.clone(), +- db_prefix: None, +- provenance: None, +- manifest_blob: None, +- }) ++ append_event( ++ &run_store, ++ &run_options.run_id, ++ &Event::RunCreated { ++ run_id: run_options.run_id, ++ settings: serde_json::to_value(&run_options.settings) ++ .expect("failed to serialize settings"), ++ graph: serde_json::to_value(graph).expect("failed to serialize graph"), ++ workflow_source: None, ++ workflow_config: None, ++ labels: run_options ++ .labels ++ .clone() ++ .into_iter() ++ .collect::>(), ++ run_dir: run_options.run_dir.display().to_string(), ++ working_directory: PathBuf::from(sandbox.working_directory()) ++ .display() ++ .to_string(), ++ host_repo_path: run_options ++ .host_repo_path ++ .as_ref() ++ .map(|path| path.display().to_string()), ++ repo_origin_url: None, ++ base_branch: run_options.base_branch.clone(), ++ workflow_slug: run_options.workflow_slug.clone(), ++ db_prefix: None, ++ provenance: None, ++ manifest_blob: None, ++ }, ++ ) + .await + .expect("failed to seed run.created event in run store"); + let emitter = bound_emitter(run_options.run_id, &emitter); +@@ -159,8 +163,8 @@ pub async fn run_graph( + run_options, + InitializedOptions { + hook_runner: None, +- env: HashMap::new(), +- checkpoint: None, ++ env: HashMap::new(), ++ checkpoint: None, + }, + ) + .await; +@@ -183,8 +187,8 @@ pub async fn run_graph_with_state( + run_options, + InitializedOptions { + hook_runner: None, +- env: HashMap::new(), +- checkpoint: None, ++ env: HashMap::new(), ++ checkpoint: None, + }, + ) + .await; +@@ -216,8 +220,8 @@ pub async fn run_graph_with_hooks( + run_options, + InitializedOptions { + hook_runner: Some(hook_runner), +- env: env.unwrap_or_default(), +- checkpoint: None, ++ env: env.unwrap_or_default(), ++ checkpoint: None, + }, + ) + .await; +@@ -242,8 +246,8 @@ pub async fn run_graph_with_hooks_and_state( + run_options, + InitializedOptions { + hook_runner: Some(hook_runner), +- env: env.unwrap_or_default(), +- checkpoint: None, ++ env: env.unwrap_or_default(), ++ checkpoint: None, + }, + ) + .await; +@@ -274,8 +278,8 @@ pub async fn run_graph_from_checkpoint( + run_options, + InitializedOptions { + hook_runner: None, +- env: HashMap::new(), +- checkpoint: Some(checkpoint.clone()), ++ env: HashMap::new(), ++ checkpoint: Some(checkpoint.clone()), + }, + ) + .await; +@@ -299,8 +303,8 @@ pub async fn run_graph_from_checkpoint_with_state( + run_options, + InitializedOptions { + hook_runner: None, +- env: HashMap::new(), +- checkpoint: Some(checkpoint.clone()), ++ env: HashMap::new(), ++ checkpoint: Some(checkpoint.clone()), + }, + ) + .await; +@@ -317,8 +321,8 @@ pub async fn run_graph_from_checkpoint_with_state( + + pub struct WorkflowRunner { + registry: std::sync::Mutex>, +- emitter: Arc, +- sandbox: Arc, ++ emitter: Arc, ++ sandbox: Arc, + } + + impl WorkflowRunner { +diff --git a/lib/crates/fabro-workflow/src/transforms/file_inlining.rs b/lib/crates/fabro-workflow/src/transforms/file_inlining.rs +index fe1df53b..2941f8f9 100644 +--- a/lib/crates/fabro-workflow/src/transforms/file_inlining.rs ++++ b/lib/crates/fabro-workflow/src/transforms/file_inlining.rs +@@ -24,7 +24,7 @@ pub fn resolve_file_ref(value: &str, current_dir: &Path, resolver: &dyn FileReso + /// Inlines `@file` references in node prompts and the graph-level goal. + pub struct FileInliningTransform { + current_dir: PathBuf, +- resolver: Arc, ++ resolver: Arc, + } + + impl FileInliningTransform { +diff --git a/lib/crates/fabro-workflow/src/transforms/import.rs b/lib/crates/fabro-workflow/src/transforms/import.rs +index 81b6db2b..da2a8502 100644 +--- a/lib/crates/fabro-workflow/src/transforms/import.rs ++++ b/lib/crates/fabro-workflow/src/transforms/import.rs +@@ -12,21 +12,21 @@ use crate::file_resolver::{FileResolver, ResolvedFile}; + + pub struct ImportTransform { + current_dir: PathBuf, +- resolver: Arc, +- inputs: HashMap, ++ resolver: Arc, ++ inputs: HashMap, + } + + struct PlaceholderOptions { +- default_attrs: HashMap, +- class_names: Vec, ++ default_attrs: HashMap, ++ class_names: Vec, + normalized_class: String, + } + + struct PreparedImport { +- graph: Graph, +- start_id: String, +- exit_id: String, +- entry_id: String, ++ graph: Graph, ++ start_id: String, ++ exit_id: String, ++ entry_id: String, + exit_predecessor_id: String, + } + +diff --git a/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs b/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs +index 3549177d..c65c3db7 100644 +--- a/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs ++++ b/lib/crates/fabro-workflow/src/transforms/variable_expansion.rs +@@ -84,8 +84,8 @@ mod tests { + graph.nodes.insert("plan".to_string(), node); + + graph.edges.push(Edge { +- from: "start".to_string(), +- to: "plan".to_string(), ++ from: "start".to_string(), ++ to: "plan".to_string(), + attrs: HashMap::from([( + "label".to_string(), + AttrValue::String("{{ inputs.greeting }}".to_string()), +diff --git a/lib/crates/fabro-workflow/src/workflow_bundle.rs b/lib/crates/fabro-workflow/src/workflow_bundle.rs +index ba5cfb64..fd7afe07 100644 +--- a/lib/crates/fabro-workflow/src/workflow_bundle.rs ++++ b/lib/crates/fabro-workflow/src/workflow_bundle.rs +@@ -9,8 +9,8 @@ use crate::file_resolver::{BundleFileResolver, FileResolver, normalize_logical_p + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + pub struct BundledWorkflow { + pub logical_path: PathBuf, +- pub source: String, +- pub files: HashMap, ++ pub source: String, ++ pub files: HashMap, + } + + impl BundledWorkflow { +@@ -63,7 +63,7 @@ impl WorkflowBundle { + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct RunDefinition { + pub workflow_path: PathBuf, +- pub workflows: HashMap, ++ pub workflows: HashMap, + } + + impl RunDefinition { +diff --git a/lib/crates/fabro-workflow/tests/it/cp_integration.rs b/lib/crates/fabro-workflow/tests/it/cp_integration.rs +index 63ff5015..421680bb 100644 +--- a/lib/crates/fabro-workflow/tests/it/cp_integration.rs ++++ b/lib/crates/fabro-workflow/tests/it/cp_integration.rs +@@ -16,11 +16,11 @@ use fabro_sandbox::reconnect::reconnect; + + fn local_record(working_directory: &std::path::Path) -> SandboxRecord { + SandboxRecord { +- provider: "local".to_string(), +- working_directory: working_directory.to_string_lossy().to_string(), +- identifier: None, ++ provider: "local".to_string(), ++ working_directory: working_directory.to_string_lossy().to_string(), ++ identifier: None, + host_working_directory: None, +- container_mount_point: None, ++ container_mount_point: None, + } + } + +@@ -118,11 +118,11 @@ async fn local_cp_creates_parent_dirs() { + + fn docker_record(host_dir: &std::path::Path, mount_point: &str) -> SandboxRecord { + SandboxRecord { +- provider: "docker".to_string(), +- working_directory: mount_point.to_string(), +- identifier: None, ++ provider: "docker".to_string(), ++ working_directory: mount_point.to_string(), ++ identifier: None, + host_working_directory: Some(host_dir.to_string_lossy().to_string()), +- container_mount_point: Some(mount_point.to_string()), ++ container_mount_point: Some(mount_point.to_string()), + } + } + +diff --git a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs +index dc41f9e4..e22bcb74 100644 +--- a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs ++++ b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs +@@ -360,10 +360,10 @@ async fn daytona_snapshot_sandbox() { + let config = DaytonaConfig { + auto_stop_interval: Some(60), + snapshot: Some(DaytonaSnapshotConfig { +- name: "fabro-test-snapshot".to_string(), +- cpu: Some(2), +- memory: Some(4), +- disk: Some(10), ++ name: "fabro-test-snapshot".to_string(), ++ cpu: Some(2), ++ memory: Some(4), ++ disk: Some(10), + dockerfile: Some(fabro_sandbox::daytona::DockerfileSource::Inline( + "FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y ripgrep".to_string(), + )), +@@ -510,17 +510,17 @@ async fn daytona_pipeline_artifact_offload_and_sync() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), env.clone()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -688,19 +688,19 @@ async fn daytona_git_checkpoint_remote_emits_events() { + + let engine = WorkflowRunner::new(registry, Arc::new(emitter), env.clone()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("git-cp-test"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("git-cp-test"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: Some(dir.path().to_path_buf()), +- git: Some(GitCheckpointOptions { +- base_sha: Some(base_sha), +- run_branch: Some(branch_name), ++ host_repo_path: Some(dir.path().to_path_buf()), ++ git: Some(GitCheckpointOptions { ++ base_sha: Some(base_sha), ++ run_branch: Some(branch_name), + meta_branch: None, + }), + }; +@@ -870,8 +870,8 @@ async fn daytona_parallel_git_branching_e2e() { + display_base_sha: None, + host_repo_path: Some(run_tmp.path().to_path_buf()), + git: Some(GitCheckpointOptions { +- base_sha: Some(base_sha), +- run_branch: Some(branch_name), ++ base_sha: Some(base_sha), ++ run_branch: Some(branch_name), + meta_branch: None, + }), + }; +@@ -989,10 +989,10 @@ async fn run_daytona_cli_test(provider: Provider, model: &str, install_command: + let creds = load_github_app_credentials(); + let config = DaytonaConfig { + snapshot: Some(DaytonaSnapshotConfig { +- name: "daytona-medium".into(), +- cpu: None, +- memory: None, +- disk: None, ++ name: "daytona-medium".into(), ++ cpu: None, ++ memory: None, ++ disk: None, + dockerfile: None, + }), + ..DaytonaConfig::default() +@@ -1219,8 +1219,8 @@ async fn daytona_git_checkpoint_with_shadow_branch() { + display_base_sha: None, + host_repo_path: Some(host_repo.path().to_path_buf()), + git: Some(GitCheckpointOptions { +- base_sha: Some(base_sha), +- run_branch: Some(branch_name), ++ base_sha: Some(base_sha), ++ run_branch: Some(branch_name), + meta_branch: Some(meta_branch), + }), + }; +@@ -1342,7 +1342,7 @@ async fn daytona_asset_collection() { + graph.edges.push(Edge::new("create_assets", "exit")); + + let run_options = RunOptions { +- settings: SettingsLayer { ++ settings: SettingsLayer { + run: Some(RunLayer { + artifacts: Some(RunArtifactsLayer { + include: vec!["test-results/**".to_string()], +@@ -1351,16 +1351,16 @@ async fn daytona_asset_collection() { + }), + ..SettingsLayer::default() + }, +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("artifact-test-daytona"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("artifact-test-daytona"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -1620,8 +1620,8 @@ async fn daytona_git_push_run_branch_to_origin() { + display_base_sha: None, + host_repo_path: Some(dir.path().to_path_buf()), + git: Some(GitCheckpointOptions { +- base_sha: Some(base_sha), +- run_branch: Some(branch_name.clone()), ++ base_sha: Some(base_sha), ++ run_branch: Some(branch_name.clone()), + meta_branch: None, + }), + }; +@@ -1824,11 +1824,11 @@ async fn daytona_cp_upload_download_round_trip() { + + // 2. Build a SandboxRecord (same as `fabro run` would persist) + let record = SandboxRecord { +- provider: "daytona".to_string(), +- working_directory: env.working_directory().to_string(), +- identifier: Some(sandbox_name.clone()), ++ provider: "daytona".to_string(), ++ working_directory: env.working_directory().to_string(), ++ identifier: Some(sandbox_name.clone()), + host_working_directory: None, +- container_mount_point: None, ++ container_mount_point: None, + }; + + // 3. Reconnect via the real cp::reconnect path +@@ -1902,10 +1902,10 @@ async fn daytona_computer_use_browser_screenshot() { + use base64::Engine; + let config = DaytonaConfig { + snapshot: Some(DaytonaSnapshotConfig { +- name: "daytona-medium".into(), +- cpu: None, +- memory: None, +- disk: None, ++ name: "daytona-medium".into(), ++ cpu: None, ++ memory: None, ++ disk: None, + dockerfile: None, + }), + skip_clone: true, +@@ -2056,10 +2056,10 @@ async fn daytona_playwright_mcp_sandbox_transport() { + // Create sandbox from daytona-medium (has Node.js + Chromium) + let config = DaytonaConfig { + snapshot: Some(DaytonaSnapshotConfig { +- name: "daytona-medium".into(), +- cpu: None, +- memory: None, +- disk: None, ++ name: "daytona-medium".into(), ++ cpu: None, ++ memory: None, ++ disk: None, + dockerfile: None, + }), + skip_clone: true, +@@ -2101,8 +2101,8 @@ async fn daytona_playwright_mcp_sandbox_transport() { + // 2. Start the Playwright MCP server via the sandbox transport resolution path + let mcp_port = 3100u16; + let mcp_config = fabro_mcp::config::McpServerSettings { +- name: "playwright".into(), +- transport: fabro_mcp::config::McpTransport::Sandbox { ++ name: "playwright".into(), ++ transport: fabro_mcp::config::McpTransport::Sandbox { + command: vec![ + "npx".into(), + "@playwright/mcp@latest".into(), +@@ -2112,11 +2112,11 @@ async fn daytona_playwright_mcp_sandbox_transport() { + "--browser".into(), + "chromium".into(), + ], +- port: mcp_port, +- env: std::collections::HashMap::new(), ++ port: mcp_port, ++ env: std::collections::HashMap::new(), + }, + startup_timeout_secs: 30, +- tool_timeout_secs: 120, ++ tool_timeout_secs: 120, + }; + + // Resolve the sandbox transport: start the server, get preview URL, rewrite to +@@ -2169,10 +2169,10 @@ async fn daytona_playwright_mcp_sandbox_transport() { + eprintln!("Preview URL: {url}"); + + fabro_mcp::config::McpServerSettings { +- name: mcp_config.name.clone(), +- transport: fabro_mcp::config::McpTransport::Http { url, headers }, ++ name: mcp_config.name.clone(), ++ transport: fabro_mcp::config::McpTransport::Http { url, headers }, + startup_timeout_secs: mcp_config.startup_timeout_secs, +- tool_timeout_secs: mcp_config.tool_timeout_secs, ++ tool_timeout_secs: mcp_config.tool_timeout_secs, + } + } + _ => unreachable!(), +diff --git a/lib/crates/fabro-workflow/tests/it/git_integration.rs b/lib/crates/fabro-workflow/tests/it/git_integration.rs +index 72b50820..2c76be5a 100644 +--- a/lib/crates/fabro-workflow/tests/it/git_integration.rs ++++ b/lib/crates/fabro-workflow/tests/it/git_integration.rs +@@ -150,17 +150,17 @@ fn make_registry() -> HandlerRegistry { + + fn test_run_options(run_dir: &Path) -> RunOptions { + RunOptions { +- run_dir: run_dir.to_path_buf(), +- cancel_token: None, +- run_id: fixtures::RUN_2, +- settings: SettingsLayer::default(), +- git: None, +- host_repo_path: None, +- labels: HashMap::new(), +- github_app: None, +- base_branch: None, ++ run_dir: run_dir.to_path_buf(), ++ cancel_token: None, ++ run_id: fixtures::RUN_2, ++ settings: SettingsLayer::default(), ++ git: None, ++ host_repo_path: None, ++ labels: HashMap::new(), ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- workflow_slug: None, ++ workflow_slug: None, + } + } + +@@ -280,8 +280,8 @@ async fn git_checkpoint_skips_start_node() { + + let mut run_options = test_run_options(run_tmp.path()); + run_options.git = Some(GitCheckpointOptions { +- base_sha: Some(base_sha), +- run_branch: None, ++ base_sha: Some(base_sha), ++ run_branch: None, + meta_branch: Some(MetadataStore::branch_name(&fixtures::RUN_2.to_string())), + }); + run_options.host_repo_path = Some(PathBuf::from(repo)); +diff --git a/lib/crates/fabro-workflow/tests/it/integration.rs b/lib/crates/fabro-workflow/tests/it/integration.rs +index 5a30a0a4..6d2ab2a1 100644 +--- a/lib/crates/fabro-workflow/tests/it/integration.rs ++++ b/lib/crates/fabro-workflow/tests/it/integration.rs +@@ -350,17 +350,17 @@ async fn end_to_end_linear_pipeline() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -479,17 +479,17 @@ async fn end_to_end_branching_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -584,9 +584,9 @@ async fn end_to_end_human_gate_pipeline() { + + // Pre-fill the queue with an answer selecting "R" + let answers = VecDeque::from([Answer { +- value: AnswerValue::Selected("R".to_string()), ++ value: AnswerValue::Selected("R".to_string()), + selected_option: None, +- text: None, ++ text: None, + }]); + let interviewer = Arc::new(QueueInterviewer::new(answers)); + +@@ -598,17 +598,17 @@ async fn end_to_end_human_gate_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -693,17 +693,17 @@ async fn human_gate_interrupted_input_fails_closed_without_fail_route() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -803,17 +803,17 @@ async fn human_gate_interrupted_input_routes_via_outcome_fail_condition() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -916,17 +916,17 @@ async fn goal_gate_routes_to_retry_target_on_failure() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!( +@@ -1036,17 +1036,17 @@ async fn goal_gate_routes_to_retry_target_when_present() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -1350,17 +1350,17 @@ async fn retry_on_failure_then_succeed() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -1424,17 +1424,17 @@ async fn pipeline_with_many_nodes() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -1515,13 +1515,13 @@ impl CodergenBackend for MockCodergenBackend { + _tool_hooks: Option>, + ) -> Result { + Ok(CodergenResult::Text { +- text: format!( ++ text: format!( + "Response for {}: processed prompt '{}'", + node.id, + &prompt[..prompt.len().min(50)] + ), +- usage: None, +- files_touched: Vec::new(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -1587,7 +1587,7 @@ impl Handler for LargeOutputHandler { + #[derive(Clone)] + struct ContextValueCaptureHandler { + values: Arc>>, +- key: String, ++ key: String, + } + + #[async_trait::async_trait] +@@ -1770,17 +1770,17 @@ async fn smoke_test_with_mock_codergen_backend() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -1871,17 +1871,17 @@ async fn end_to_end_parallel_fan_out_fan_in() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -1983,17 +1983,17 @@ async fn resume_from_checkpoint_completes_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_from_checkpoint_with_state(&graph, &run_options, &checkpoint) +@@ -2081,17 +2081,17 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + // This should succeed because goal gate for gated_work is satisfied + // via restored outcomes +@@ -2123,17 +2123,17 @@ async fn graph_goal_in_context() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -2161,17 +2161,17 @@ async fn event_streaming_lifecycle() { + let events = collect_events(&emitter); + let engine = WorkflowRunner::new(make_linear_registry(), Arc::new(emitter), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -2240,17 +2240,17 @@ async fn context_flow_between_stages() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -2295,17 +2295,17 @@ async fn tool_handler_e2e() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -2369,17 +2369,17 @@ async fn auto_approve_interviewer_e2e() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -2408,17 +2408,17 @@ async fn codergen_without_backend_simulated() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -2512,17 +2512,17 @@ async fn branching_loop_back_on_failure() { + ); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -2578,14 +2578,14 @@ async fn human_gate_loops_back() { + + let answers = VecDeque::from([ + Answer { +- value: AnswerValue::Selected("F".to_string()), ++ value: AnswerValue::Selected("F".to_string()), + selected_option: None, +- text: None, ++ text: None, + }, + Answer { +- value: AnswerValue::Selected("A".to_string()), ++ value: AnswerValue::Selected("A".to_string()), + selected_option: None, +- text: None, ++ text: None, + }, + ]); + let interviewer = Arc::new(QueueInterviewer::new(answers)); +@@ -2597,17 +2597,17 @@ async fn human_gate_loops_back() { + registry.register("human", Box::new(HumanHandler::new(interviewer))); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -2661,17 +2661,17 @@ async fn scenario_ship_a_feature() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -2745,17 +2745,17 @@ async fn scenario_parallel_expert_review() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -2831,17 +2831,17 @@ async fn scenario_node_retries_on_retry_status() { + ); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -2895,17 +2895,17 @@ async fn scenario_loop_restart_resets_context() { + ); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine.run(&graph, &run_options).await.expect("run"); + assert_eq!(outcome.status, StageStatus::Success); +@@ -2962,17 +2962,17 @@ async fn scenario_bug_triage_router() { + registry.register("conditional", Box::new(ConditionalHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3023,17 +3023,17 @@ async fn scenario_crash_recovery() { + registry.register("exit", Box::new(ExitHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_from_checkpoint_with_state(&graph, &run_options, &checkpoint) +@@ -3132,17 +3132,17 @@ async fn manager_loop_stop_condition_satisfied_e2e() { + registry.register("stack.manager_loop", Box::new(SubWorkflowHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3213,17 +3213,17 @@ async fn manager_loop_max_cycles_exceeded_e2e() { + registry.register("stack.manager_loop", Box::new(SubWorkflowHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3353,17 +3353,17 @@ async fn conditional_branching_success_fail_paths() { + registry.register("always_fail", Box::new(AlwaysFailHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3408,17 +3408,17 @@ async fn edge_selection_condition_match_wins_over_weight() { + registry.register("exit", Box::new(ExitHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3457,17 +3457,17 @@ async fn edge_selection_weight_breaks_ties() { + registry.register("exit", Box::new(ExitHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3498,17 +3498,17 @@ async fn edge_selection_lexical_tiebreak() { + registry.register("exit", Box::new(ExitHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3558,17 +3558,17 @@ async fn context_updates_visible_across_nodes() { + registry.register("context_setter", Box::new(ContextSetterHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3604,17 +3604,17 @@ async fn stylesheet_applies_model_override() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine.run(&graph, &run_options).await.expect("run"); + assert_eq!(outcome.status, StageStatus::Success); +@@ -3659,17 +3659,17 @@ async fn custom_handler_registration_and_execution() { + registry.register("my_custom", Box::new(CustomHandler)); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3736,17 +3736,17 @@ async fn integration_smoke_plan_implement_review_done() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3827,17 +3827,17 @@ async fn manager_loop_runs_child_engine_e2e() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -3960,17 +3960,17 @@ async fn manager_loop_context_flows_e2e() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -4035,17 +4035,17 @@ async fn manager_loop_child_dotfile_e2e() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine.run(&graph, &run_options).await.expect("run"); + assert_eq!(outcome.status, StageStatus::Success); +@@ -4094,14 +4094,17 @@ async fn import_e2e_through_engine() { + }"#, + ) + .expect("parse should succeed"); +- let transformed = transform(parsed, &TransformOptions { +- current_dir: Some(dir.path().to_path_buf()), +- file_resolver: Some(std::sync::Arc::new( +- fabro_workflow::file_resolver::FilesystemFileResolver::new(None), +- )), +- inputs: std::collections::HashMap::new(), +- custom_transforms: vec![], +- }) ++ let transformed = transform( ++ parsed, ++ &TransformOptions { ++ current_dir: Some(dir.path().to_path_buf()), ++ file_resolver: Some(std::sync::Arc::new( ++ fabro_workflow::file_resolver::FilesystemFileResolver::new(None), ++ )), ++ inputs: std::collections::HashMap::new(), ++ custom_transforms: vec![], ++ }, ++ ) + .unwrap(); + let validated = validate(transformed, &[]); + validated +@@ -4138,17 +4141,17 @@ async fn import_e2e_through_engine() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -4210,7 +4213,7 @@ type SharedVec = Arc>>; + struct FidelityCaptures { + fidelities: SharedVec<(String, String)>, + thread_ids: SharedVec<(String, Option)>, +- preambles: SharedVec<(String, String)>, ++ preambles: SharedVec<(String, String)>, + } + + impl FidelityCaptures { +@@ -4218,7 +4221,7 @@ impl FidelityCaptures { + Self { + fidelities: Arc::new(std::sync::Mutex::new(Vec::new())), + thread_ids: Arc::new(std::sync::Mutex::new(Vec::new())), +- preambles: Arc::new(std::sync::Mutex::new(Vec::new())), ++ preambles: Arc::new(std::sync::Mutex::new(Vec::new())), + } + } + } +@@ -4292,17 +4295,17 @@ async fn fidelity_default_is_compact() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4348,17 +4351,17 @@ async fn fidelity_graph_default_applied() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4400,17 +4403,17 @@ async fn fidelity_node_overrides_graph_default() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4458,17 +4461,17 @@ async fn fidelity_edge_overrides_node_and_graph() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4506,17 +4509,17 @@ async fn fidelity_full_produces_empty_preamble() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4564,17 +4567,17 @@ async fn fidelity_truncate_preamble_minimal() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4635,17 +4638,17 @@ async fn fidelity_summary_low_mode() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4701,17 +4704,17 @@ async fn fidelity_summary_medium_mode() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4767,17 +4770,17 @@ async fn fidelity_summary_high_mode() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4826,17 +4829,17 @@ async fn fidelity_full_sets_thread_id_in_context() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4896,17 +4899,17 @@ async fn fidelity_full_nodes_share_thread_id() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -4976,17 +4979,17 @@ async fn fidelity_resume_degrades_full_to_summary_high() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine + .run_from_checkpoint(&graph, &run_options, &checkpoint) +@@ -5072,17 +5075,17 @@ async fn fidelity_resume_degrade_only_affects_first_hop() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine + .run_from_checkpoint(&graph, &run_options, &checkpoint) +@@ -5155,17 +5158,17 @@ async fn fidelity_resume_no_degrade_when_not_full() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine + .run_from_checkpoint(&graph, &run_options, &checkpoint) +@@ -5196,17 +5199,17 @@ async fn fidelity_stored_in_checkpoint_context() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -5288,17 +5291,17 @@ async fn fidelity_precedence_multi_node_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -5355,17 +5358,17 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -5429,17 +5432,17 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { + ); + let engine_low = WorkflowRunner::new(registry_low, Arc::new(Emitter::default()), local_env()); + let run_options_low = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir_low.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir_low.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine_low + .run(&graph_low, &run_options_low) +@@ -5495,17 +5498,17 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { + ); + let engine_med = WorkflowRunner::new(registry_med, Arc::new(Emitter::default()), local_env()); + let run_options_med = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir_med.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir_med.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine_med + .run(&graph_med, &run_options_med) +@@ -5566,17 +5569,17 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -5619,17 +5622,17 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -5675,17 +5678,17 @@ async fn fidelity_edge_thread_id_override_in_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -5732,17 +5735,17 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -5799,17 +5802,17 @@ async fn fidelity_from_parsed_dot_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -5847,17 +5850,17 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -5919,17 +5922,17 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + engine.run(&graph, &run_options).await.expect("run"); + +@@ -6005,17 +6008,17 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_from_checkpoint_with_state(&graph, &run_options, &checkpoint) +@@ -6056,8 +6059,8 @@ mod real_llm { + use fabro_workflow::handler::agent::{AgentHandler, CodergenBackend, CodergenResult}; + + struct LlmCodergenBackend { +- client: Arc, +- model: String, ++ client: Arc, ++ model: String, + provider: String, + } + +@@ -6089,19 +6092,19 @@ mod real_llm { + impl LlmCodergenBackend { + async fn complete(&self, prompt: &str) -> Result { + let request = Request { +- model: self.model.clone(), +- messages: vec![Message::user(prompt)], +- provider: Some(self.provider.clone()), +- tools: None, +- tool_choice: None, +- response_format: None, +- temperature: Some(0.0), +- top_p: None, +- max_tokens: Some(200), +- stop_sequences: None, ++ model: self.model.clone(), ++ messages: vec![Message::user(prompt)], ++ provider: Some(self.provider.clone()), ++ tools: None, ++ tool_choice: None, ++ response_format: None, ++ temperature: Some(0.0), ++ top_p: None, ++ max_tokens: Some(200), ++ stop_sequences: None, + reasoning_effort: None, +- speed: None, +- metadata: None, ++ speed: None, ++ metadata: None, + provider_options: None, + }; + let response = self +@@ -6110,9 +6113,9 @@ mod real_llm { + .await + .map_err(|e| Error::handler(e.to_string()))?; + Ok(CodergenResult::Text { +- text: response.text(), +- usage: None, +- files_touched: Vec::new(), ++ text: response.text(), ++ usage: None, ++ files_touched: Vec::new(), + last_file_touched: None, + }) + } +@@ -6241,17 +6244,17 @@ mod real_llm { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = tokio::time::timeout( + std::time::Duration::from_secs(120), +@@ -6349,17 +6352,17 @@ mod real_llm { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = tokio::time::timeout( + std::time::Duration::from_secs(120), +@@ -6481,17 +6484,17 @@ mod real_llm { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = tokio::time::timeout( + std::time::Duration::from_secs(120), +@@ -6581,17 +6584,17 @@ mod real_llm { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = tokio::time::timeout( + std::time::Duration::from_secs(30), +@@ -6674,17 +6677,17 @@ async fn human_gate_freeform_only_routes_text() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -6790,9 +6793,9 @@ async fn human_gate_freeform_with_fixed_choice_match() { + + // Answer selects "A" which matches the Approve choice + let answers = VecDeque::from([Answer { +- value: AnswerValue::Selected("A".to_string()), ++ value: AnswerValue::Selected("A".to_string()), + selected_option: None, +- text: None, ++ text: None, + }]); + let interviewer = Arc::new(QueueInterviewer::new(answers)); + +@@ -6804,17 +6807,17 @@ async fn human_gate_freeform_with_fixed_choice_match() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -6920,17 +6923,17 @@ async fn human_gate_freeform_fallback_on_unmatched_text() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -7031,9 +7034,9 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { + graph.edges.push(Edge::new("freeform_target", "exit")); + + let answers = VecDeque::from([Answer { +- value: AnswerValue::Selected("A".to_string()), ++ value: AnswerValue::Selected("A".to_string()), + selected_option: None, +- text: None, ++ text: None, + }]); + let inner = QueueInterviewer::new(answers); + let recorder = Arc::new(RecordingInterviewer::new(Box::new(inner))); +@@ -7047,17 +7050,17 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -7139,9 +7142,9 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { + graph.edges.push(Edge::new("reject", "exit")); + + let answers = VecDeque::from([Answer { +- value: AnswerValue::Selected("A".to_string()), ++ value: AnswerValue::Selected("A".to_string()), + selected_option: None, +- text: None, ++ text: None, + }]); + let inner = QueueInterviewer::new(answers); + let recorder = Arc::new(RecordingInterviewer::new(Box::new(inner))); +@@ -7155,17 +7158,17 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -7391,7 +7394,7 @@ fn hook_runner_from_defs(hooks: Vec) -> Arc, ++ emitter: Arc, + hook_runner: Arc, + } + +@@ -7437,7 +7440,7 @@ fn emitter_with_events() -> (Arc, Arc>>) + + fn engine_with_hooks(hooks: Vec) -> HookTestRunner { + HookTestRunner { +- emitter: Arc::new(Emitter::default()), ++ emitter: Arc::new(Emitter::default()), + hook_runner: hook_runner_from_defs(hooks), + } + } +@@ -7457,17 +7460,17 @@ fn engine_with_hooks_and_events( + + fn make_run_options(dir: &std::path::Path) -> RunOptions { + RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("hook-test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("hook-test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + } + } + +@@ -8052,26 +8055,26 @@ async fn hook_config_merge_concatenates() { + + let server_hooks = HookSettings { + hooks: vec![HookDefinition { +- name: Some("server-hook".into()), +- event: HookEvent::RunStart, +- command: Some("exit 0".into()), +- hook_type: None, +- matcher: None, +- blocking: None, ++ name: Some("server-hook".into()), ++ event: HookEvent::RunStart, ++ command: Some("exit 0".into()), ++ hook_type: None, ++ matcher: None, ++ blocking: None, + timeout_ms: None, +- sandbox: Some(false), ++ sandbox: Some(false), + }], + }; + let run_hooks = HookSettings { + hooks: vec![HookDefinition { +- name: Some("run-hook".into()), +- event: HookEvent::StageComplete, +- command: Some("exit 0".into()), +- hook_type: None, +- matcher: None, +- blocking: None, ++ name: Some("run-hook".into()), ++ event: HookEvent::StageComplete, ++ command: Some("exit 0".into()), ++ hook_type: None, ++ matcher: None, ++ blocking: None, + timeout_ms: None, +- sandbox: Some(false), ++ sandbox: Some(false), + }], + }; + +@@ -8087,26 +8090,26 @@ async fn hook_config_merge_run_overrides_by_name() { + + let server_hooks = HookSettings { + hooks: vec![HookDefinition { +- name: Some("shared".into()), +- event: HookEvent::RunStart, +- command: Some("exit 1".into()), // would block +- hook_type: None, +- matcher: None, +- blocking: None, ++ name: Some("shared".into()), ++ event: HookEvent::RunStart, ++ command: Some("exit 1".into()), // would block ++ hook_type: None, ++ matcher: None, ++ blocking: None, + timeout_ms: None, +- sandbox: Some(false), ++ sandbox: Some(false), + }], + }; + let run_hooks = HookSettings { + hooks: vec![HookDefinition { +- name: Some("shared".into()), +- event: HookEvent::RunStart, +- command: Some("exit 0".into()), // allows +- hook_type: None, +- matcher: None, +- blocking: None, ++ name: Some("shared".into()), ++ event: HookEvent::RunStart, ++ command: Some("exit 0".into()), // allows ++ hook_type: None, ++ matcher: None, ++ blocking: None, + timeout_ms: None, +- sandbox: Some(false), ++ sandbox: Some(false), + }], + }; + +@@ -8395,17 +8398,17 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -8596,17 +8599,17 @@ async fn large_context_values_are_offloaded_to_artifact_store() { + let events = collect_events(&emitter); + let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -8655,16 +8658,16 @@ async fn large_context_values_are_offloaded_to_artifact_store() { + /// A mock sandbox where `file_exists` always returns false, + /// simulating a remote container that doesn't have local artifact files. + struct RemoteMockEnv { +- working_dir: String, +- written: std::sync::Mutex>, ++ working_dir: String, ++ written: std::sync::Mutex>, + existing_paths: std::sync::Mutex>, + } + + impl RemoteMockEnv { + fn new(working_dir: &str) -> Self { + Self { +- working_dir: working_dir.to_string(), +- written: std::sync::Mutex::new(Vec::new()), ++ working_dir: working_dir.to_string(), ++ written: std::sync::Mutex::new(Vec::new()), + existing_paths: std::sync::Mutex::new(std::collections::HashSet::new()), + } + } +@@ -8800,17 +8803,17 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() { + let remote_env = Arc::new(RemoteMockEnv::new("/sandbox")); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), remote_env.clone()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -8881,23 +8884,23 @@ async fn downstream_local_execution_materializes_blob_refs_to_runtime_files() { + "capture_context", + Box::new(ContextValueCaptureHandler { + values: Arc::clone(&captured), +- key: "response.big_output".to_string(), ++ key: "response.big_output".to_string(), + }), + ); + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -8967,24 +8970,24 @@ async fn downstream_remote_execution_materializes_blob_refs_to_sandbox_files() { + "capture_context", + Box::new(ContextValueCaptureHandler { + values: Arc::clone(&captured), +- key: "response.big_output".to_string(), ++ key: "response.big_output".to_string(), + }), + ); + + let remote_env = Arc::new(RemoteMockEnv::new("/sandbox")); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), remote_env.clone()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, _state) = engine + .run_with_state(&graph, &run_options) +@@ -9104,17 +9107,17 @@ async fn node_dir_uses_visit_count_on_revisit() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -9149,25 +9152,25 @@ async fn node_dir_uses_visit_count_on_revisit() { + /// responses based on command content. + struct CliTestEnv { + /// All commands passed to exec_command, in order. +- commands: std::sync::Mutex>, ++ commands: std::sync::Mutex>, + /// All (path, content) pairs from write_file. +- written_files: std::sync::Mutex>, ++ written_files: std::sync::Mutex>, + /// The stdout to return when the CLI command (not git) is executed. +- cli_stdout: String, ++ cli_stdout: String, + /// Files returned by "git diff --name-only" AFTER the CLI runs. + /// First call returns empty (before), second returns these (after). + git_diff_call_count: std::sync::atomic::AtomicU32, +- git_diff_after: String, ++ git_diff_after: String, + } + + impl CliTestEnv { + fn new(cli_stdout: &str) -> Self { + Self { +- commands: std::sync::Mutex::new(Vec::new()), +- written_files: std::sync::Mutex::new(Vec::new()), +- cli_stdout: cli_stdout.to_string(), ++ commands: std::sync::Mutex::new(Vec::new()), ++ written_files: std::sync::Mutex::new(Vec::new()), ++ cli_stdout: cli_stdout.to_string(), + git_diff_call_count: std::sync::atomic::AtomicU32::new(0), +- git_diff_after: String::new(), ++ git_diff_after: String::new(), + } + } + +@@ -9254,10 +9257,10 @@ impl fabro_agent::Sandbox for CliTestEnv { + // Background launch: return PID + if command.contains("echo $!") { + return Ok(fabro_agent::ExecResult { +- stdout: "12345\n".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "12345\n".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 1, + }); + } +@@ -9265,10 +9268,10 @@ impl fabro_agent::Sandbox for CliTestEnv { + // Poll for completion: return exit code 0 immediately + if command.contains("exit_code") && command.contains("echo running") { + return Ok(fabro_agent::ExecResult { +- stdout: "0\n".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "0\n".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 1, + }); + } +@@ -9276,10 +9279,10 @@ impl fabro_agent::Sandbox for CliTestEnv { + // Read stdout file + if command.starts_with("cat") && command.contains("stdout.log") { + return Ok(fabro_agent::ExecResult { +- stdout: self.cli_stdout.clone(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: self.cli_stdout.clone(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 1, + }); + } +@@ -9287,10 +9290,10 @@ impl fabro_agent::Sandbox for CliTestEnv { + // Read stderr file + if command.starts_with("cat") && command.contains("stderr.log") { + return Ok(fabro_agent::ExecResult { +- stdout: String::new(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: String::new(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 1, + }); + } +@@ -9298,20 +9301,20 @@ impl fabro_agent::Sandbox for CliTestEnv { + // Cleanup temp files + if command.starts_with("rm -f") { + return Ok(fabro_agent::ExecResult { +- stdout: String::new(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: String::new(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 1, + }); + } + + // Fallback + Ok(fabro_agent::ExecResult { +- stdout: self.cli_stdout.clone(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: self.cli_stdout.clone(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 100, + }) + } +@@ -9547,48 +9550,48 @@ async fn cli_backend_run_fails_on_nonzero_exit() { + ) -> Result { + if command.starts_with("git") { + return Ok(fabro_agent::ExecResult { +- stdout: String::new(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: String::new(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 0, + }); + } + // Background launch: return PID + if command.contains("echo $!") { + return Ok(fabro_agent::ExecResult { +- stdout: "12345\n".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "12345\n".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 0, + }); + } + // Poll: return non-zero exit code + if command.contains("exit_code") && command.contains("echo running") { + return Ok(fabro_agent::ExecResult { +- stdout: "127\n".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "127\n".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 0, + }); + } + // Read stderr file + if command.starts_with("cat") && command.contains("stderr.log") { + return Ok(fabro_agent::ExecResult { +- stdout: "command not found: claude".into(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: "command not found: claude".into(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 0, + }); + } + Ok(fabro_agent::ExecResult { +- stdout: String::new(), +- stderr: String::new(), +- exit_code: 0, +- timed_out: false, ++ stdout: String::new(), ++ stderr: String::new(), ++ exit_code: 0, ++ timed_out: false, + duration_ms: 0, + }) + } +@@ -9974,17 +9977,17 @@ async fn full_pipeline_with_cli_backend_node() { + let dir = tempfile::tempdir().unwrap(); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), env); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -10092,17 +10095,17 @@ async fn stylesheet_backend_property_routes_to_cli() { + let dir = tempfile::tempdir().unwrap(); + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), env); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine + .run_with_state(&graph, &run_options) +@@ -10287,19 +10290,19 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() { + let engine = WorkflowRunner::new(registry, Arc::new(emitter), env); + + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: run_dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-docker"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: run_dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-docker"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: Some(worktree_path.clone()), +- git: Some(GitCheckpointOptions { +- base_sha: Some(base_sha.clone()), +- run_branch: Some(run_branch), ++ host_repo_path: Some(worktree_path.clone()), ++ git: Some(GitCheckpointOptions { ++ base_sha: Some(base_sha.clone()), ++ run_branch: Some(run_branch), + meta_branch: None, + }), + }; +@@ -10465,8 +10468,8 @@ async fn git_checkpoint_host_writes_shadow_branch() { + display_base_sha: None, + host_repo_path: Some(worktree_path.clone()), + git: Some(GitCheckpointOptions { +- base_sha: Some(base_sha), +- run_branch: Some(format!("fabro/run/{run_id}")), ++ base_sha: Some(base_sha), ++ run_branch: Some(format!("fabro/run/{run_id}")), + meta_branch: Some(meta_branch), + }), + }; +@@ -10664,8 +10667,8 @@ async fn parallel_git_branching_host_e2e() { + display_base_sha: None, + host_repo_path: Some(worktree_path.clone()), + git: Some(GitCheckpointOptions { +- base_sha: Some(base_sha.clone()), +- run_branch: Some(run_branch.clone()), ++ base_sha: Some(base_sha.clone()), ++ run_branch: Some(run_branch.clone()), + meta_branch: None, + }), + }; +@@ -10902,19 +10905,19 @@ async fn git_checkpoint_host_skips_empty_diff_patch() { + let engine = WorkflowRunner::new(registry, Arc::new(emitter), env); + + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: run_dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("empty-diff"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: run_dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("empty-diff"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: Some(worktree_path.clone()), +- git: Some(GitCheckpointOptions { +- base_sha: Some(base_sha.clone()), +- run_branch: Some(run_branch), ++ host_repo_path: Some(worktree_path.clone()), ++ git: Some(GitCheckpointOptions { ++ base_sha: Some(base_sha.clone()), ++ run_branch: Some(run_branch), + meta_branch: None, + }), + }; +@@ -11043,7 +11046,7 @@ impl Handler for VaryingReasonFailHandler { + /// before that. + struct SucceedOnNthHandler { + succeed_on: u32, +- counter: std::sync::atomic::AtomicU32, ++ counter: std::sync::atomic::AtomicU32, + } + + #[async_trait::async_trait] +@@ -11272,17 +11275,17 @@ async fn e2e_circuit_breaker_deterministic_self_loop() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-circuit-breaker"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-circuit-breaker"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!(result.is_err(), "pipeline should abort, not loop forever"); +@@ -11318,17 +11321,17 @@ async fn e2e_circuit_breaker_custom_limit() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-custom-limit"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-custom-limit"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!(result.is_err()); +@@ -11357,17 +11360,17 @@ async fn e2e_circuit_breaker_ignores_transient_failures() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-transient-no-breaker"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-transient-no-breaker"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!(result.is_err()); +@@ -11403,17 +11406,17 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-varying-reasons"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-varying-reasons"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!(result.is_err()); +@@ -11442,17 +11445,17 @@ async fn e2e_circuit_breaker_loop_restart() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-restart-breaker"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-restart-breaker"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!( +@@ -11504,17 +11507,17 @@ async fn e2e_failure_signature_persisted_in_context() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-sig-context"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-sig-context"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine.run_with_state(&graph, &run_options).await.unwrap(); + // Pipeline reaches exit (terminal) with goal gates satisfied. +@@ -11567,17 +11570,17 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-sig-hint"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-sig-hint"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (_outcome, state) = engine.run_with_state(&graph, &run_options).await.unwrap(); + +@@ -11618,23 +11621,23 @@ async fn e2e_signature_maps_persist_in_checkpoint() { + "test_handler", + Box::new(SucceedOnNthHandler { + succeed_on: 3, +- counter: std::sync::atomic::AtomicU32::new(0), ++ counter: std::sync::atomic::AtomicU32::new(0), + }), + ); + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-sig-persist"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-sig-persist"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine.run_with_state(&graph, &run_options).await.unwrap(); + assert_eq!(outcome.status, StageStatus::Success); +@@ -11751,17 +11754,17 @@ async fn e2e_circuit_breaker_emits_events_before_abort() { + + let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-events"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-events"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!(result.is_err()); +@@ -11811,23 +11814,23 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() { + "test_handler", + Box::new(SucceedOnNthHandler { + succeed_on: 4, +- counter: std::sync::atomic::AtomicU32::new(0), ++ counter: std::sync::atomic::AtomicU32::new(0), + }), + ); + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-below-limit"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-below-limit"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let (outcome, state) = engine.run_with_state(&graph, &run_options).await.unwrap(); + assert_eq!( +@@ -11912,17 +11915,17 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-impl-verify-cycle"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-impl-verify-cycle"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!( +@@ -11946,8 +11949,8 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() { + /// Nth call. + struct ClassifiedFailHandler { + failure_class: &'static str, +- succeed_on: u32, +- counter: std::sync::atomic::AtomicU32, ++ succeed_on: u32, ++ counter: std::sync::atomic::AtomicU32, + } + + impl ClassifiedFailHandler { +@@ -12009,17 +12012,17 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-restart-blocked-det"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-restart-blocked-det"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!( +@@ -12048,17 +12051,17 @@ async fn e2e_loop_restart_blocked_for_structural_failure() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-restart-blocked-struct"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-restart-blocked-struct"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!( +@@ -12087,17 +12090,17 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-restart-blocked-budget"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-restart-blocked-budget"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!( +@@ -12126,17 +12129,17 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-restart-blocked-canceled"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-restart-blocked-canceled"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!(result.is_err(), "canceled failure should not loop_restart"); +@@ -12162,17 +12165,17 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-restart-blocked-comploop"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-restart-blocked-comploop"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!( +@@ -12202,17 +12205,17 @@ async fn e2e_loop_restart_allowed_for_transient_infra() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("e2e-restart-allowed-transient"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("e2e-restart-allowed-transient"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!( +@@ -12247,7 +12250,7 @@ impl Handler for HangingHandler { + /// Handler that emits keepalive events periodically, then succeeds. + struct KeepaliveHandler { + interval_ms: u64, +- total_ms: u64, ++ total_ms: u64, + } + + #[async_trait::async_trait] +@@ -12264,12 +12267,12 @@ impl Handler for KeepaliveHandler { + while start.elapsed() < std::time::Duration::from_millis(self.total_ms) { + tokio::time::sleep(std::time::Duration::from_millis(self.interval_ms)).await; + services.emitter.emit(&Event::Prompt { +- stage: node.id.clone(), +- visit: 1, +- text: "keepalive".to_string(), +- mode: None, ++ stage: node.id.clone(), ++ visit: 1, ++ text: "keepalive".to_string(), ++ mode: None, + provider: None, +- model: None, ++ model: None, + }); + } + Ok(Outcome::success()) +@@ -12309,17 +12312,17 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() { + + let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("stall-e2e"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("stall-e2e"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let result = engine.run(&graph, &run_options).await; + assert!(result.is_err(), "expected stall watchdog error"); +@@ -12358,23 +12361,23 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() { + "keepalive", + Box::new(KeepaliveHandler { + interval_ms: 10, +- total_ms: 50, ++ total_ms: 50, + }), + ); + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("stall-alive-e2e"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("stall-alive-e2e"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -12409,17 +12412,17 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("stall-disabled-e2e"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("stall-disabled-e2e"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -12474,17 +12477,17 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() { + + let engine = WorkflowRunner::new(registry, Arc::new(Emitter::default()), local_env()); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("stall-override-e2e"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("stall-override-e2e"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let start = std::time::Instant::now(); + let result = engine.run(&graph, &run_options).await; +@@ -12606,7 +12609,7 @@ async fn asset_collection_local_sandbox_success() { + graph.edges.push(Edge::new("create_assets", "exit")); + + let run_options = RunOptions { +- settings: SettingsLayer { ++ settings: SettingsLayer { + run: Some(RunLayer { + artifacts: Some(RunArtifactsLayer { + include: vec!["test-results/**".to_string()], +@@ -12615,16 +12618,16 @@ async fn asset_collection_local_sandbox_success() { + }), + ..SettingsLayer::default() + }, +- run_dir: run_dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("artifact-test-local"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ run_dir: run_dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("artifact-test-local"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -12738,7 +12741,7 @@ async fn asset_collection_local_sandbox_on_failure() { + graph.edges.push(Edge::new("create_assets", "exit")); + + let run_options = RunOptions { +- settings: SettingsLayer { ++ settings: SettingsLayer { + run: Some(RunLayer { + artifacts: Some(RunArtifactsLayer { + include: vec!["test-results/**".to_string()], +@@ -12747,16 +12750,16 @@ async fn asset_collection_local_sandbox_on_failure() { + }), + ..SettingsLayer::default() + }, +- run_dir: run_dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("artifact-test-fail"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ run_dir: run_dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("artifact-test-fail"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -12843,7 +12846,7 @@ async fn asset_collection_docker_sandbox() { + graph.edges.push(Edge::new("create_assets", "exit")); + + let run_options = RunOptions { +- settings: SettingsLayer { ++ settings: SettingsLayer { + run: Some(RunLayer { + artifacts: Some(RunArtifactsLayer { + include: vec!["test-results/**".to_string()], +@@ -12852,16 +12855,16 @@ async fn asset_collection_docker_sandbox() { + }), + ..SettingsLayer::default() + }, +- run_dir: run_dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("artifact-test-docker"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ run_dir: run_dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("artifact-test-docker"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine + .run(&graph, &run_options) +@@ -12919,17 +12922,17 @@ async fn wait_timer_e2e() { + local_env(), + ); + let run_options = RunOptions { +- settings: SettingsLayer::default(), +- run_dir: dir.path().to_path_buf(), +- cancel_token: None, +- run_id: test_run_id("test-run"), +- labels: std::collections::HashMap::new(), +- workflow_slug: None, +- github_app: None, +- base_branch: None, ++ settings: SettingsLayer::default(), ++ run_dir: dir.path().to_path_buf(), ++ cancel_token: None, ++ run_id: test_run_id("test-run"), ++ labels: std::collections::HashMap::new(), ++ workflow_slug: None, ++ github_app: None, ++ base_branch: None, + display_base_sha: None, +- host_repo_path: None, +- git: None, ++ host_repo_path: None, ++ git: None, + }; + let outcome = engine.run(&graph, &run_options).await.expect("run"); + assert_eq!(outcome.status, StageStatus::Success); +diff --git a/test/twin/github/src/fixtures.rs b/test/twin/github/src/fixtures.rs +index bd341b55..ba67c9ab 100644 +--- a/test/twin/github/src/fixtures.rs ++++ b/test/twin/github/src/fixtures.rs +@@ -14,92 +14,92 @@ use crate::state::{ + #[derive(Debug, Clone, Deserialize, Default)] + pub struct FixtureState { + #[serde(default)] +- pub apps: Vec, ++ pub apps: Vec, + #[serde(default)] +- pub installations: Vec, ++ pub installations: Vec, + #[serde(default)] +- pub repositories: Vec, ++ pub repositories: Vec, + #[serde(default)] +- pub pull_requests: Vec, ++ pub pull_requests: Vec, + #[serde(default)] +- pub active_tokens: Vec, ++ pub active_tokens: Vec, + #[serde(default)] +- pub projects: Vec, ++ pub projects: Vec, + #[serde(default)] +- pub releases: Vec, ++ pub releases: Vec, + #[serde(default)] + pub manifest_conversions: Vec, + #[serde(default)] +- pub comments: Vec, ++ pub comments: Vec, + #[serde(default)] +- pub webhook_config: FixtureWebhookOptions, ++ pub webhook_config: FixtureWebhookOptions, + pub next_installation_id: Option, +- pub next_pr_number: Option, +- pub viewer_id: Option, ++ pub next_pr_number: Option, ++ pub viewer_id: Option, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureApp { +- pub app_id: String, +- pub slug: String, +- pub owner_login: String, +- pub public: bool, ++ pub app_id: String, ++ pub slug: String, ++ pub owner_login: String, ++ pub public: bool, + pub private_key_pem: String, +- pub webhook_secret: Option, ++ pub webhook_secret: Option, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureInstallation { +- pub id: u64, +- pub app_id: String, +- pub owner: String, ++ pub id: u64, ++ pub app_id: String, ++ pub owner: String, + #[serde(default)] + pub repositories: Vec, + #[serde(default)] +- pub suspended: bool, ++ pub suspended: bool, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureRepository { +- pub owner: String, +- pub name: String, ++ pub owner: String, ++ pub name: String, + #[serde(default)] +- pub branches: Vec, ++ pub branches: Vec, + pub default_branch: Option, + #[serde(default)] +- pub private: bool, ++ pub private: bool, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixturePullRequest { +- pub owner: String, +- pub repo: String, ++ pub owner: String, ++ pub repo: String, + #[serde(flatten)] + pub pull_request: PullRequest, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureActiveToken { +- pub token: String, +- pub app_id: String, ++ pub token: String, ++ pub app_id: String, + pub installation_id: u64, + #[serde(default)] +- pub repositories: Vec, ++ pub repositories: Vec, + #[serde(default = "empty_json_object")] +- pub permissions: serde_json::Value, ++ pub permissions: serde_json::Value, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureProject { +- pub node_id: String, +- pub number: u64, +- pub owner: String, +- pub owner_type: FixtureOwnerType, ++ pub node_id: String, ++ pub number: u64, ++ pub owner: String, ++ pub owner_type: FixtureOwnerType, + pub status_field_id: String, + #[serde(default)] +- pub status_options: Vec, ++ pub status_options: Vec, + #[serde(default)] +- pub items: Vec, ++ pub items: Vec, + } + + #[derive(Debug, Clone, Deserialize)] +@@ -111,59 +111,59 @@ pub enum FixtureOwnerType { + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureStatusOption { +- pub id: String, ++ pub id: String, + pub name: String, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureProjectItem { +- pub id: String, +- pub status: String, ++ pub id: String, ++ pub status: String, + pub content: FixtureIssueContent, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureIssueContent { +- pub id: String, +- pub number: u64, +- pub title: String, +- pub body: String, +- pub url: String, +- pub created_at: String, +- pub updated_at: String, ++ pub id: String, ++ pub number: u64, ++ pub title: String, ++ pub body: String, ++ pub url: String, ++ pub created_at: String, ++ pub updated_at: String, + #[serde(default)] + pub assignee_ids: Vec, + #[serde(default)] +- pub labels: Vec, ++ pub labels: Vec, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureRelease { +- pub owner: String, +- pub repo: String, ++ pub owner: String, ++ pub repo: String, + pub tag_name: String, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureManifestConversion { +- pub code: String, +- pub app_id: i64, +- pub slug: String, +- pub client_id: String, +- pub client_secret: String, ++ pub code: String, ++ pub app_id: i64, ++ pub slug: String, ++ pub client_id: String, ++ pub client_secret: String, + pub webhook_secret: Option, +- pub pem: String, ++ pub pem: String, + } + + #[derive(Debug, Clone, Deserialize)] + pub struct FixtureComment { + pub issue_node_id: String, +- pub body: String, ++ pub body: String, + } + + #[derive(Debug, Clone, Deserialize, Default)] + pub struct FixtureWebhookOptions { +- pub url: Option, ++ pub url: Option, + pub content_type: Option, + } + +@@ -182,12 +182,12 @@ impl FixtureState { + for app in self.apps { + let app_id = app.app_id.clone(); + let config = AppOptions { +- app_id: app.app_id, +- slug: app.slug, +- owner_login: app.owner_login, +- public: app.public, ++ app_id: app.app_id, ++ slug: app.slug, ++ owner_login: app.owner_login, ++ public: app.public, + private_key_pem: app.private_key_pem, +- webhook_secret: app.webhook_secret, ++ webhook_secret: app.webhook_secret, + }; + + catch_unwind(AssertUnwindSafe(|| state.register_app(config))) +@@ -198,11 +198,11 @@ impl FixtureState { + .installations + .into_iter() + .map(|installation| Installation { +- id: installation.id, +- app_id: installation.app_id, +- owner: installation.owner, ++ id: installation.id, ++ app_id: installation.app_id, ++ owner: installation.owner, + repositories: installation.repositories, +- suspended: installation.suspended, ++ suspended: installation.suspended, + }) + .collect(); + +@@ -210,14 +210,14 @@ impl FixtureState { + .repositories + .into_iter() + .map(|repository| Repository { +- owner: repository.owner, +- name: repository.name, +- branches: repository.branches, ++ owner: repository.owner, ++ name: repository.name, ++ branches: repository.branches, + default_branch: repository + .default_branch + .unwrap_or_else(|| "main".to_string()), +- private: repository.private, +- git_dir: None, ++ private: repository.private, ++ git_dir: None, + }) + .collect(); + +@@ -233,12 +233,15 @@ impl FixtureState { + .active_tokens + .into_iter() + .map(|token| { +- (token.token, TokenInfo { +- app_id: token.app_id, +- installation_id: token.installation_id, +- repositories: token.repositories, +- permissions: token.permissions, +- }) ++ ( ++ token.token, ++ TokenInfo { ++ app_id: token.app_id, ++ installation_id: token.installation_id, ++ repositories: token.repositories, ++ permissions: token.permissions, ++ }, ++ ) + }) + .collect(); + +@@ -246,38 +249,38 @@ impl FixtureState { + .projects + .into_iter() + .map(|project| Project { +- node_id: project.node_id, +- number: project.number, +- owner: project.owner, +- owner_type: match project.owner_type { ++ node_id: project.node_id, ++ number: project.number, ++ owner: project.owner, ++ owner_type: match project.owner_type { + FixtureOwnerType::Organization => OwnerType::Organization, + FixtureOwnerType::User => OwnerType::User, + }, + status_field_id: project.status_field_id, +- status_options: project ++ status_options: project + .status_options + .into_iter() + .map(|option| StatusOption { +- id: option.id, ++ id: option.id, + name: option.name, + }) + .collect(), +- items: project ++ items: project + .items + .into_iter() + .map(|item| ProjectItem { +- id: item.id, +- status: item.status, ++ id: item.id, ++ status: item.status, + content: IssueContent { +- id: item.content.id, +- number: item.content.number, +- title: item.content.title, +- body: item.content.body, +- url: item.content.url, +- created_at: item.content.created_at, +- updated_at: item.content.updated_at, ++ id: item.content.id, ++ number: item.content.number, ++ title: item.content.title, ++ body: item.content.body, ++ url: item.content.url, ++ created_at: item.content.created_at, ++ updated_at: item.content.updated_at, + assignee_ids: item.content.assignee_ids, +- labels: item.content.labels, ++ labels: item.content.labels, + }, + }) + .collect(), +@@ -288,9 +291,12 @@ impl FixtureState { + .releases + .into_iter() + .map(|release| { +- ((release.owner, release.repo), Release { +- tag_name: release.tag_name, +- }) ++ ( ++ (release.owner, release.repo), ++ Release { ++ tag_name: release.tag_name, ++ }, ++ ) + }) + .collect::>(); + +@@ -299,15 +305,18 @@ impl FixtureState { + .into_iter() + .map(|conversion| { + let code = conversion.code.clone(); +- (code, ManifestConversion { +- code: conversion.code, +- app_id: conversion.app_id, +- slug: conversion.slug, +- client_id: conversion.client_id, +- client_secret: conversion.client_secret, +- webhook_secret: conversion.webhook_secret, +- pem: conversion.pem, +- }) ++ ( ++ code, ++ ManifestConversion { ++ code: conversion.code, ++ app_id: conversion.app_id, ++ slug: conversion.slug, ++ client_id: conversion.client_id, ++ client_secret: conversion.client_secret, ++ webhook_secret: conversion.webhook_secret, ++ pem: conversion.pem, ++ }, ++ ) + }) + .collect::>(); + +@@ -316,12 +325,12 @@ impl FixtureState { + .into_iter() + .map(|comment| Comment { + issue_node_id: comment.issue_node_id, +- body: comment.body, ++ body: comment.body, + }) + .collect(); + + state.webhook_config = WebhookOptions { +- url: self.webhook_config.url, ++ url: self.webhook_config.url, + content_type: self.webhook_config.content_type, + }; + +@@ -367,12 +376,12 @@ impl FixtureState { + fn single_app_fixture_for_test() -> Self { + Self { + apps: vec![FixtureApp { +- app_id: "100".to_string(), +- slug: "fixture-app".to_string(), +- owner_login: "acme".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "fixture-app".to_string(), ++ owner_login: "acme".to_string(), ++ public: true, + private_key_pem: test_rsa_key().to_string(), +- webhook_secret: Some("whsec".to_string()), ++ webhook_secret: Some("whsec".to_string()), + }], + ..Self::default() + } +diff --git a/test/twin/github/src/handlers/app.rs b/test/twin/github/src/handlers/app.rs +index ec67fe39..58212dcd 100644 +--- a/test/twin/github/src/handlers/app.rs ++++ b/test/twin/github/src/handlers/app.rs +@@ -120,12 +120,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "12345".to_string(), +- slug: "my-app".to_string(), +- owner_login: "my-org".to_string(), +- public: true, ++ app_id: "12345".to_string(), ++ slug: "my-app".to_string(), ++ owner_login: "my-org".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + let server = TestServer::start(state).await; + +@@ -152,12 +152,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "12345".to_string(), +- slug: "my-app".to_string(), +- owner_login: "my-org".to_string(), +- public: true, ++ app_id: "12345".to_string(), ++ slug: "my-app".to_string(), ++ owner_login: "my-org".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + let server = TestServer::start(state).await; + +@@ -178,12 +178,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "12345".to_string(), +- slug: "my-app".to_string(), +- owner_login: "my-org".to_string(), +- public: true, ++ app_id: "12345".to_string(), ++ slug: "my-app".to_string(), ++ owner_login: "my-org".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + let server = TestServer::start(state).await; + +@@ -209,12 +209,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "12345".to_string(), +- slug: "private-app".to_string(), +- owner_login: "my-org".to_string(), +- public: false, ++ app_id: "12345".to_string(), ++ slug: "private-app".to_string(), ++ owner_login: "my-org".to_string(), ++ public: false, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + let server = TestServer::start(state).await; + +diff --git a/test/twin/github/src/handlers/branches.rs b/test/twin/github/src/handlers/branches.rs +index 5eca78d4..a58a1f2d 100644 +--- a/test/twin/github/src/handlers/branches.rs ++++ b/test/twin/github/src/handlers/branches.rs +@@ -134,12 +134,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + state.add_installation("100", "owner", vec!["repo".to_string()], false); + state.add_repository( +@@ -177,12 +177,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + state.add_installation("100", "owner", vec!["repo".to_string()], false); + state.add_repository("owner", "repo", vec!["main".to_string()], false); +diff --git a/test/twin/github/src/handlers/graphql.rs b/test/twin/github/src/handlers/graphql.rs +index 1794c406..e9756da4 100644 +--- a/test/twin/github/src/handlers/graphql.rs ++++ b/test/twin/github/src/handlers/graphql.rs +@@ -160,7 +160,7 @@ async fn handle_enable_auto_merge( + } + } + pr.auto_merge = Some(AutoMerge { +- enabled_at: now.clone(), ++ enabled_at: now.clone(), + merge_method: method.clone(), + }); + return ( +@@ -665,12 +665,12 @@ mod tests { + pem: &str, + ) -> (TestServer, fabro_http::HttpClient, String) { + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + state.add_installation("100", "owner", vec!["repo".to_string()], false); + state.add_repository("owner", "repo", vec!["main".to_string()], false); +@@ -717,23 +717,23 @@ mod tests { + .entry(("owner".to_string(), "repo".to_string())) + .or_default() + .push(PullRequest { +- number: 1, +- node_id: "PR_test123".to_string(), +- title: "Test".to_string(), +- body: String::new(), +- state: "open".to_string(), +- draft: false, +- mergeable: true, +- additions: 10, +- deletions: 5, ++ number: 1, ++ node_id: "PR_test123".to_string(), ++ title: "Test".to_string(), ++ body: String::new(), ++ state: "open".to_string(), ++ draft: false, ++ mergeable: true, ++ additions: 10, ++ deletions: 5, + changed_files: 2, +- html_url: "https://github.com/owner/repo/pull/1".to_string(), +- user_login: "test-bot[bot]".to_string(), +- head_ref: "feature".to_string(), +- base_ref: "main".to_string(), +- created_at: "2026-01-01T00:00:00Z".to_string(), +- updated_at: "2026-01-01T00:00:00Z".to_string(), +- auto_merge: None, ++ html_url: "https://github.com/owner/repo/pull/1".to_string(), ++ user_login: "test-bot[bot]".to_string(), ++ head_ref: "feature".to_string(), ++ base_ref: "main".to_string(), ++ created_at: "2026-01-01T00:00:00Z".to_string(), ++ updated_at: "2026-01-01T00:00:00Z".to_string(), ++ auto_merge: None, + }); + let (server, client, token) = setup_with_token(&mut state, pem).await; + +@@ -794,38 +794,38 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.projects.push(crate::state::Project { +- node_id: "PVT_org123".to_string(), +- number: 1, +- owner: "owner".to_string(), +- owner_type: crate::state::OwnerType::Organization, ++ node_id: "PVT_org123".to_string(), ++ number: 1, ++ owner: "owner".to_string(), ++ owner_type: crate::state::OwnerType::Organization, + status_field_id: "PVTSSF_status1".to_string(), +- status_options: vec![ ++ status_options: vec![ + crate::state::StatusOption { +- id: "opt1".to_string(), ++ id: "opt1".to_string(), + name: "Todo".to_string(), + }, + crate::state::StatusOption { +- id: "opt2".to_string(), ++ id: "opt2".to_string(), + name: "In Progress".to_string(), + }, + crate::state::StatusOption { +- id: "opt3".to_string(), ++ id: "opt3".to_string(), + name: "Done".to_string(), + }, + ], +- items: vec![crate::state::ProjectItem { +- id: "PVTI_item1".to_string(), +- status: "Todo".to_string(), ++ items: vec![crate::state::ProjectItem { ++ id: "PVTI_item1".to_string(), ++ status: "Todo".to_string(), + content: crate::state::IssueContent { +- id: "I_issue1".to_string(), +- number: 42, +- title: "Fix bug".to_string(), +- body: "Description".to_string(), +- url: "https://github.com/owner/repo/issues/42".to_string(), +- created_at: "2026-01-01T00:00:00Z".to_string(), +- updated_at: "2026-01-02T00:00:00Z".to_string(), ++ id: "I_issue1".to_string(), ++ number: 42, ++ title: "Fix bug".to_string(), ++ body: "Description".to_string(), ++ url: "https://github.com/owner/repo/issues/42".to_string(), ++ created_at: "2026-01-01T00:00:00Z".to_string(), ++ updated_at: "2026-01-02T00:00:00Z".to_string(), + assignee_ids: vec![], +- labels: vec!["bug".to_string()], ++ labels: vec!["bug".to_string()], + }, + }], + }); +@@ -864,25 +864,25 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.projects.push(crate::state::Project { +- node_id: "PVT_test".to_string(), +- number: 1, +- owner: "owner".to_string(), +- owner_type: crate::state::OwnerType::Organization, ++ node_id: "PVT_test".to_string(), ++ number: 1, ++ owner: "owner".to_string(), ++ owner_type: crate::state::OwnerType::Organization, + status_field_id: "PVTSSF_s".to_string(), +- status_options: vec![], +- items: vec![crate::state::ProjectItem { +- id: "PVTI_1".to_string(), +- status: "Todo".to_string(), ++ status_options: vec![], ++ items: vec![crate::state::ProjectItem { ++ id: "PVTI_1".to_string(), ++ status: "Todo".to_string(), + content: crate::state::IssueContent { +- id: "I_1".to_string(), +- number: 1, +- title: "Issue 1".to_string(), +- body: "Body".to_string(), +- url: "https://github.com/owner/repo/issues/1".to_string(), +- created_at: "2026-01-01T00:00:00Z".to_string(), +- updated_at: "2026-01-01T00:00:00Z".to_string(), ++ id: "I_1".to_string(), ++ number: 1, ++ title: "Issue 1".to_string(), ++ body: "Body".to_string(), ++ url: "https://github.com/owner/repo/issues/1".to_string(), ++ created_at: "2026-01-01T00:00:00Z".to_string(), ++ updated_at: "2026-01-01T00:00:00Z".to_string(), + assignee_ids: vec!["U_user1".to_string()], +- labels: vec!["bug".to_string(), "urgent".to_string()], ++ labels: vec!["bug".to_string(), "urgent".to_string()], + }, + }], + }); +@@ -948,34 +948,34 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.projects.push(crate::state::Project { +- node_id: "PVT_test".to_string(), +- number: 1, +- owner: "owner".to_string(), +- owner_type: crate::state::OwnerType::Organization, ++ node_id: "PVT_test".to_string(), ++ number: 1, ++ owner: "owner".to_string(), ++ owner_type: crate::state::OwnerType::Organization, + status_field_id: "PVTSSF_s".to_string(), +- status_options: vec![ ++ status_options: vec![ + crate::state::StatusOption { +- id: "opt1".to_string(), ++ id: "opt1".to_string(), + name: "Todo".to_string(), + }, + crate::state::StatusOption { +- id: "opt2".to_string(), ++ id: "opt2".to_string(), + name: "Done".to_string(), + }, + ], +- items: vec![crate::state::ProjectItem { +- id: "PVTI_1".to_string(), +- status: "Todo".to_string(), ++ items: vec![crate::state::ProjectItem { ++ id: "PVTI_1".to_string(), ++ status: "Todo".to_string(), + content: crate::state::IssueContent { +- id: "I_1".to_string(), +- number: 1, +- title: "Issue 1".to_string(), +- body: String::new(), +- url: "https://github.com/owner/repo/issues/1".to_string(), +- created_at: "2026-01-01T00:00:00Z".to_string(), +- updated_at: "2026-01-01T00:00:00Z".to_string(), ++ id: "I_1".to_string(), ++ number: 1, ++ title: "Issue 1".to_string(), ++ body: String::new(), ++ url: "https://github.com/owner/repo/issues/1".to_string(), ++ created_at: "2026-01-01T00:00:00Z".to_string(), ++ updated_at: "2026-01-01T00:00:00Z".to_string(), + assignee_ids: vec![], +- labels: vec![], ++ labels: vec![], + }, + }], + }); +diff --git a/test/twin/github/src/handlers/installations.rs b/test/twin/github/src/handlers/installations.rs +index 9422e2a0..91bd6ca2 100644 +--- a/test/twin/github/src/handlers/installations.rs ++++ b/test/twin/github/src/handlers/installations.rs +@@ -185,12 +185,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + state.add_installation("100", "owner", vec!["repo".to_string()], false); + state.add_repository("owner", "repo", vec!["main".to_string()], false); +@@ -217,12 +217,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + // No installation added + let server = TestServer::start(state).await; +@@ -245,12 +245,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + state.add_installation("100", "owner", vec!["repo".to_string()], true); // suspended + let server = TestServer::start(state).await; +@@ -273,12 +273,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + let install_id = state.add_installation("100", "owner", vec!["repo".to_string()], false); + let server = TestServer::start(state).await; +@@ -311,12 +311,12 @@ mod tests { + let pem = test_rsa_private_key(); + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + let install_id = state.add_installation("100", "owner", vec!["repo".to_string()], false); + let server = TestServer::start(state).await; +diff --git a/test/twin/github/src/handlers/manifests.rs b/test/twin/github/src/handlers/manifests.rs +index c7711078..a9fca6a6 100644 +--- a/test/twin/github/src/handlers/manifests.rs ++++ b/test/twin/github/src/handlers/manifests.rs +@@ -41,19 +41,19 @@ mod tests { + #[tokio::test] + async fn manifest_conversion_returns_app_credentials() { + let mut state = AppState::new(); +- state +- .manifest_conversions +- .insert("test-code".to_string(), ManifestConversion { +- code: "test-code".to_string(), +- app_id: 99, +- slug: "test-dev".to_string(), +- client_id: "Iv1.abc123".to_string(), +- client_secret: "secret123".to_string(), ++ state.manifest_conversions.insert( ++ "test-code".to_string(), ++ ManifestConversion { ++ code: "test-code".to_string(), ++ app_id: 99, ++ slug: "test-dev".to_string(), ++ client_id: "Iv1.abc123".to_string(), ++ client_secret: "secret123".to_string(), + webhook_secret: Some("whsecret".to_string()), +- pem: +- "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----" +- .to_string(), +- }); ++ pem: "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----" ++ .to_string(), ++ }, ++ ); + let server = TestServer::start(state).await; + + let client = crate::test_support::test_http_client(); +diff --git a/test/twin/github/src/handlers/pulls.rs b/test/twin/github/src/handlers/pulls.rs +index 5dcbcbb7..6e93ddf1 100644 +--- a/test/twin/github/src/handlers/pulls.rs ++++ b/test/twin/github/src/handlers/pulls.rs +@@ -344,12 +344,12 @@ mod tests { + pem: &str, + ) -> (TestServer, fabro_http::HttpClient, String) { + state.register_app(AppOptions { +- app_id: "100".to_string(), +- slug: "test-app".to_string(), +- owner_login: "owner".to_string(), +- public: true, ++ app_id: "100".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "owner".to_string(), ++ public: true, + private_key_pem: pem.to_string(), +- webhook_secret: None, ++ webhook_secret: None, + }); + state.add_installation("100", "owner", vec!["repo".to_string()], false); + state.add_repository( +diff --git a/test/twin/github/src/server.rs b/test/twin/github/src/server.rs +index 89fc9341..d06d38cf 100644 +--- a/test/twin/github/src/server.rs ++++ b/test/twin/github/src/server.rs +@@ -12,10 +12,10 @@ pub type SharedState = Arc>; + + /// A running test server instance. + pub struct TestServer { +- url: String, ++ url: String, + shutdown_tx: Option>, +- handle: Option>, +- _git_root: TempDir, // Kept alive for the server's lifetime; cleaned up on drop ++ handle: Option>, ++ _git_root: TempDir, // Kept alive for the server's lifetime; cleaned up on drop + } + + impl TestServer { +diff --git a/test/twin/github/src/state.rs b/test/twin/github/src/state.rs +index 21764dd5..d7494e53 100644 +--- a/test/twin/github/src/state.rs ++++ b/test/twin/github/src/state.rs +@@ -8,18 +8,18 @@ const TEST_RSA_PUBLIC_PEM: &str = include_str!("testdata/rsa_public.pem"); + /// Configuration for a registered GitHub App (user-facing input). + #[derive(Debug, Clone)] + pub struct AppOptions { +- pub app_id: String, +- pub slug: String, +- pub owner_login: String, +- pub public: bool, ++ pub app_id: String, ++ pub slug: String, ++ pub owner_login: String, ++ pub public: bool, + pub private_key_pem: String, +- pub webhook_secret: Option, ++ pub webhook_secret: Option, + } + + /// Internal enriched app config with derived public key. + #[derive(Debug, Clone)] + pub struct RegisteredApp { +- pub config: AppOptions, ++ pub config: AppOptions, + /// Derived from `private_key_pem` during `register_app`. Used for JWT + /// verification. + pub public_key_pem: String, +@@ -28,62 +28,62 @@ pub struct RegisteredApp { + /// An installation of a GitHub App on a specific owner. + #[derive(Debug, Clone)] + pub struct Installation { +- pub id: u64, +- pub app_id: String, +- pub owner: String, ++ pub id: u64, ++ pub app_id: String, ++ pub owner: String, + pub repositories: Vec, +- pub suspended: bool, ++ pub suspended: bool, + } + + /// A repository in the fake. + #[derive(Debug, Clone)] + pub struct Repository { +- pub owner: String, +- pub name: String, +- pub branches: Vec, ++ pub owner: String, ++ pub name: String, ++ pub branches: Vec, + pub default_branch: String, +- pub private: bool, +- pub git_dir: Option, ++ pub private: bool, ++ pub git_dir: Option, + } + + /// A pull request. + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct PullRequest { +- pub number: u64, +- pub node_id: String, +- pub title: String, +- pub body: String, +- pub state: String, +- pub draft: bool, +- pub mergeable: bool, +- pub additions: u64, +- pub deletions: u64, ++ pub number: u64, ++ pub node_id: String, ++ pub title: String, ++ pub body: String, ++ pub state: String, ++ pub draft: bool, ++ pub mergeable: bool, ++ pub additions: u64, ++ pub deletions: u64, + pub changed_files: u64, +- pub html_url: String, +- pub user_login: String, +- pub head_ref: String, +- pub base_ref: String, +- pub created_at: String, +- pub updated_at: String, +- pub auto_merge: Option, ++ pub html_url: String, ++ pub user_login: String, ++ pub head_ref: String, ++ pub base_ref: String, ++ pub created_at: String, ++ pub updated_at: String, ++ pub auto_merge: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct AutoMerge { +- pub enabled_at: String, ++ pub enabled_at: String, + pub merge_method: String, + } + + /// A GitHub Projects V2 project. + #[derive(Debug, Clone)] + pub struct Project { +- pub node_id: String, +- pub number: u64, +- pub owner: String, +- pub owner_type: OwnerType, ++ pub node_id: String, ++ pub number: u64, ++ pub owner: String, ++ pub owner_type: OwnerType, + pub status_field_id: String, +- pub status_options: Vec, +- pub items: Vec, ++ pub status_options: Vec, ++ pub items: Vec, + } + + #[derive(Debug, Clone, PartialEq)] +@@ -94,28 +94,28 @@ pub enum OwnerType { + + #[derive(Debug, Clone)] + pub struct StatusOption { +- pub id: String, ++ pub id: String, + pub name: String, + } + + #[derive(Debug, Clone)] + pub struct ProjectItem { +- pub id: String, +- pub status: String, ++ pub id: String, ++ pub status: String, + pub content: IssueContent, + } + + #[derive(Debug, Clone)] + pub struct IssueContent { +- pub id: String, +- pub number: u64, +- pub title: String, +- pub body: String, +- pub url: String, +- pub created_at: String, +- pub updated_at: String, ++ pub id: String, ++ pub number: u64, ++ pub title: String, ++ pub body: String, ++ pub url: String, ++ pub created_at: String, ++ pub updated_at: String, + pub assignee_ids: Vec, +- pub labels: Vec, ++ pub labels: Vec, + } + + /// A release. +@@ -127,26 +127,26 @@ pub struct Release { + /// An app manifest conversion record. + #[derive(Debug, Clone)] + pub struct ManifestConversion { +- pub code: String, +- pub app_id: i64, +- pub slug: String, +- pub client_id: String, +- pub client_secret: String, ++ pub code: String, ++ pub app_id: i64, ++ pub slug: String, ++ pub client_id: String, ++ pub client_secret: String, + pub webhook_secret: Option, +- pub pem: String, ++ pub pem: String, + } + + /// A comment on an issue. + #[derive(Debug, Clone)] + pub struct Comment { + pub issue_node_id: String, +- pub body: String, ++ pub body: String, + } + + /// Stores webhook configuration. + #[derive(Debug, Clone, Default)] + pub struct WebhookOptions { +- pub url: Option, ++ pub url: Option, + pub content_type: Option, + } + +@@ -178,18 +178,18 @@ pub enum TokenPermission { + + #[derive(Debug, Clone, PartialEq, Eq, Default)] + pub struct TokenPermissions { +- pub contents: PermissionLevel, +- pub pull_requests: PermissionLevel, +- pub issues: PermissionLevel, ++ pub contents: PermissionLevel, ++ pub pull_requests: PermissionLevel, ++ pub issues: PermissionLevel, + pub organization_projects: PermissionLevel, + } + + impl TokenPermissions { + pub fn from_json(value: &serde_json::Value) -> Self { + Self { +- contents: PermissionLevel::from_json(value.get("contents")), +- pull_requests: PermissionLevel::from_json(value.get("pull_requests")), +- issues: PermissionLevel::from_json(value.get("issues")), ++ contents: PermissionLevel::from_json(value.get("contents")), ++ pull_requests: PermissionLevel::from_json(value.get("pull_requests")), ++ issues: PermissionLevel::from_json(value.get("issues")), + organization_projects: PermissionLevel::from_json(value.get("organization_projects")), + } + } +@@ -207,10 +207,10 @@ impl TokenPermissions { + /// Info about an active installation access token. + #[derive(Debug, Clone)] + pub struct TokenInfo { +- pub app_id: String, ++ pub app_id: String, + pub installation_id: u64, +- pub repositories: Vec, +- pub permissions: serde_json::Value, ++ pub repositories: Vec, ++ pub permissions: serde_json::Value, + } + + impl TokenInfo { +@@ -230,19 +230,19 @@ impl TokenInfo { + /// Central in-memory state for the fake GitHub server. + #[derive(Debug, Clone)] + pub struct AppState { +- pub apps: HashMap, +- pub installations: Vec, +- pub repositories: Vec, +- pub pull_requests: HashMap<(String, String), Vec>, +- pub active_tokens: HashMap, +- pub projects: Vec, +- pub releases: HashMap<(String, String), Release>, ++ pub apps: HashMap, ++ pub installations: Vec, ++ pub repositories: Vec, ++ pub pull_requests: HashMap<(String, String), Vec>, ++ pub active_tokens: HashMap, ++ pub projects: Vec, ++ pub releases: HashMap<(String, String), Release>, + pub manifest_conversions: HashMap, +- pub comments: Vec, +- pub webhook_config: WebhookOptions, ++ pub comments: Vec, ++ pub webhook_config: WebhookOptions, + pub next_installation_id: u64, +- pub next_pr_number: u64, +- pub viewer_id: String, ++ pub next_pr_number: u64, ++ pub viewer_id: String, + } + + /// Derive the RSA public key PEM from a private key PEM using the openssl CLI. +@@ -283,29 +283,32 @@ pub fn derive_public_key_pem(private_key_pem: &str) -> String { + impl AppState { + pub fn new() -> Self { + Self { +- apps: HashMap::new(), +- installations: Vec::new(), +- repositories: Vec::new(), +- pull_requests: HashMap::new(), +- active_tokens: HashMap::new(), +- projects: Vec::new(), +- releases: HashMap::new(), ++ apps: HashMap::new(), ++ installations: Vec::new(), ++ repositories: Vec::new(), ++ pull_requests: HashMap::new(), ++ active_tokens: HashMap::new(), ++ projects: Vec::new(), ++ releases: HashMap::new(), + manifest_conversions: HashMap::new(), +- comments: Vec::new(), +- webhook_config: WebhookOptions::default(), ++ comments: Vec::new(), ++ webhook_config: WebhookOptions::default(), + next_installation_id: 1, +- next_pr_number: 1, +- viewer_id: "U_fakeviewer".to_string(), ++ next_pr_number: 1, ++ viewer_id: "U_fakeviewer".to_string(), + } + } + + pub fn register_app(&mut self, config: AppOptions) { + let public_key_pem = derive_public_key_pem(&config.private_key_pem); + let app_id = config.app_id.clone(); +- self.apps.insert(app_id, RegisteredApp { +- config, +- public_key_pem, +- }); ++ self.apps.insert( ++ app_id, ++ RegisteredApp { ++ config, ++ public_key_pem, ++ }, ++ ); + } + + pub fn add_installation( +@@ -362,12 +365,15 @@ impl AppState { + permissions: serde_json::Value, + ) -> String { + let token = format!("ghs_{}", uuid::Uuid::new_v4().to_string().replace('-', "")); +- self.active_tokens.insert(token.clone(), TokenInfo { +- app_id: app_id.to_string(), +- installation_id, +- repositories, +- permissions, +- }); ++ self.active_tokens.insert( ++ token.clone(), ++ TokenInfo { ++ app_id: app_id.to_string(), ++ installation_id, ++ repositories, ++ permissions, ++ }, ++ ); + token + } + +@@ -477,12 +483,12 @@ mod tests { + fn can_register_app() { + let mut state = AppState::new(); + state.register_app(AppOptions { +- app_id: "12345".to_string(), +- slug: "test-app".to_string(), +- owner_login: "test-owner".to_string(), +- public: true, ++ app_id: "12345".to_string(), ++ slug: "test-app".to_string(), ++ owner_login: "test-owner".to_string(), ++ public: true, + private_key_pem: test_rsa_private_key().to_string(), +- webhook_secret: Some("secret".to_string()), ++ webhook_secret: Some("secret".to_string()), + }); + assert_eq!(state.apps.len(), 1); + assert_eq!(state.apps["12345"].config.slug, "test-app"); +diff --git a/test/twin/openai/src/config.rs b/test/twin/openai/src/config.rs +index 1bb77929..73598f11 100644 +--- a/test/twin/openai/src/config.rs ++++ b/test/twin/openai/src/config.rs +@@ -4,7 +4,7 @@ use anyhow::{Context, Result}; + + #[derive(Clone, Debug)] + pub struct Config { +- pub bind_addr: SocketAddr, ++ pub bind_addr: SocketAddr, + pub require_auth: bool, + pub enable_admin: bool, + } +@@ -40,7 +40,7 @@ impl Config { + impl Default for Config { + fn default() -> Self { + Self::from_env().unwrap_or(Self { +- bind_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3000), ++ bind_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3000), + require_auth: true, + enable_admin: true, + }) +diff --git a/test/twin/openai/src/engine/failures.rs b/test/twin/openai/src/engine/failures.rs +index 8ad78f60..04559788 100644 +--- a/test/twin/openai/src/engine/failures.rs ++++ b/test/twin/openai/src/engine/failures.rs +@@ -6,22 +6,22 @@ use crate::openai::models::{ErrorBody, ErrorEnvelope}; + #[derive(Clone, Copy, Debug, Default)] + pub struct TransportOptions { + pub delay_before_headers_ms: u64, +- pub inter_event_delay_ms: u64, +- pub close_after_chunks: Option, +- pub malformed_sse: bool, ++ pub inter_event_delay_ms: u64, ++ pub close_after_chunks: Option, ++ pub malformed_sse: bool, + } + + #[derive(Clone, Debug)] + pub struct SuccessOutcome { +- pub plan: ResponsePlan, ++ pub plan: ResponsePlan, + pub transport: TransportOptions, + } + + #[derive(Clone, Debug)] + pub struct ErrorOutcome { +- pub status: StatusCode, +- pub body: ErrorEnvelope, +- pub retry_after: Option, ++ pub status: StatusCode, ++ pub body: ErrorEnvelope, ++ pub retry_after: Option, + pub delay_before_headers_ms: u64, + } + +diff --git a/test/twin/openai/src/engine/mod.rs b/test/twin/openai/src/engine/mod.rs +index 56359ecd..ad011a19 100644 +--- a/test/twin/openai/src/engine/mod.rs ++++ b/test/twin/openai/src/engine/mod.rs +@@ -19,10 +19,10 @@ pub fn execute_responses_request( + ) -> Result { + request.validate()?; + let context = RequestContext { +- endpoint: "responses".to_owned(), +- model: request.model.clone(), +- stream: request.stream, +- metadata: request.metadata.clone(), ++ endpoint: "responses".to_owned(), ++ model: request.model.clone(), ++ stream: request.stream, ++ metadata: request.metadata.clone(), + input_text: request.extract_user_text(), + }; + state.log_request(namespace, context.clone()); +@@ -39,7 +39,7 @@ pub fn execute_responses_request( + Ok(ExecutionOutcome::Success(enforce_tool_choice( + request.tool_choice_mode(), + SuccessOutcome { +- plan: build_default_response_plan(state.next_response_id(namespace), request), ++ plan: build_default_response_plan(state.next_response_id(namespace), request), + transport: TransportOptions::default(), + }, + )?)) +@@ -52,10 +52,10 @@ pub fn execute_chat_request( + ) -> Result { + request.validate()?; + let context = RequestContext { +- endpoint: "chat.completions".to_owned(), +- model: request.model.clone(), +- stream: request.stream, +- metadata: serde_json::Map::new(), ++ endpoint: "chat.completions".to_owned(), ++ model: request.model.clone(), ++ stream: request.stream, ++ metadata: serde_json::Map::new(), + input_text: request.extract_user_text(), + }; + state.log_request(namespace, context.clone()); +@@ -72,7 +72,7 @@ pub fn execute_chat_request( + Ok(ExecutionOutcome::Success(enforce_tool_choice( + request.tool_choice_mode(), + SuccessOutcome { +- plan: build_default_chat_plan( ++ plan: build_default_chat_plan( + state.next_response_id(namespace), + request.model.clone(), + &request.extract_user_text(), +diff --git a/test/twin/openai/src/engine/plan.rs b/test/twin/openai/src/engine/plan.rs +index 054078eb..0d0455ce 100644 +--- a/test/twin/openai/src/engine/plan.rs ++++ b/test/twin/openai/src/engine/plan.rs +@@ -2,21 +2,21 @@ use serde_json::{Value, json}; + + #[derive(Clone, Debug)] + pub struct ResponsePlan { +- pub id: String, +- pub created: u64, +- pub model: String, +- pub response_text: String, ++ pub id: String, ++ pub created: u64, ++ pub model: String, ++ pub response_text: String, + pub structured_output: Option, +- pub reasoning: Vec, +- pub tool_calls: Vec, +- pub input_tokens: u64, +- pub output_tokens: u64, ++ pub reasoning: Vec, ++ pub tool_calls: Vec, ++ pub input_tokens: u64, ++ pub output_tokens: u64, + } + + #[derive(Clone, Debug)] + pub struct ToolCallPlan { +- pub id: String, +- pub name: String, ++ pub id: String, ++ pub name: String, + pub arguments: Value, + } + +diff --git a/test/twin/openai/src/engine/scenario.rs b/test/twin/openai/src/engine/scenario.rs +index 59d8fd64..2c0b812d 100644 +--- a/test/twin/openai/src/engine/scenario.rs ++++ b/test/twin/openai/src/engine/scenario.rs +@@ -14,16 +14,16 @@ pub struct ScenarioEnvelope { + #[derive(Clone, Debug, Deserialize)] + pub struct Scenario { + pub matcher: ScenarioMatcher, +- pub script: ScenarioScript, ++ pub script: ScenarioScript, + } + + #[derive(Clone, Debug, Deserialize)] + pub struct ScenarioMatcher { +- pub endpoint: String, +- pub model: Option, +- pub stream: Option, ++ pub endpoint: String, ++ pub model: Option, ++ pub stream: Option, + #[serde(default)] +- pub metadata: Map, ++ pub metadata: Map, + pub input_contains: Option, + } + +@@ -31,21 +31,21 @@ pub struct ScenarioMatcher { + #[serde(tag = "kind", rename_all = "snake_case")] + pub enum ScenarioScript { + Success { +- response_text: Option, +- reasoning: Option>, +- structured_output: Option, +- tool_calls: Option>, ++ response_text: Option, ++ reasoning: Option>, ++ structured_output: Option, ++ tool_calls: Option>, + delay_before_headers_ms: Option, +- inter_event_delay_ms: Option, +- close_after_chunks: Option, +- malformed_sse: Option, ++ inter_event_delay_ms: Option, ++ close_after_chunks: Option, ++ malformed_sse: Option, + }, + Error { +- status: u16, +- message: String, +- error_type: String, +- code: String, +- retry_after: Option, ++ status: u16, ++ message: String, ++ error_type: String, ++ code: String, ++ retry_after: Option, + delay_before_headers_ms: Option, + }, + Hang { +@@ -55,17 +55,17 @@ pub enum ScenarioScript { + + #[derive(Clone, Debug, Deserialize, Serialize)] + pub struct ToolCallTemplate { +- pub id: Option, +- pub name: String, ++ pub id: Option, ++ pub name: String, + pub arguments: Value, + } + + #[derive(Clone, Debug)] + pub struct RequestContext { +- pub endpoint: String, +- pub model: String, +- pub stream: bool, +- pub metadata: Map, ++ pub endpoint: String, ++ pub model: String, ++ pub stream: bool, ++ pub metadata: Map, + pub input_text: String, + } + +@@ -127,7 +127,7 @@ impl Scenario { + close_after_chunks, + malformed_sse, + } => ExecutionOutcome::Success(SuccessOutcome { +- plan: build_plan_from_script( ++ plan: build_plan_from_script( + response_number, + request.model.clone(), + &request.extract_user_text(), +@@ -138,9 +138,9 @@ impl Scenario { + ), + transport: TransportOptions { + delay_before_headers_ms: delay_before_headers_ms.unwrap_or_default(), +- inter_event_delay_ms: inter_event_delay_ms.unwrap_or_default(), +- close_after_chunks: *close_after_chunks, +- malformed_sse: malformed_sse.unwrap_or(false), ++ inter_event_delay_ms: inter_event_delay_ms.unwrap_or_default(), ++ close_after_chunks: *close_after_chunks, ++ malformed_sse: malformed_sse.unwrap_or(false), + }, + }), + ScenarioScript::Error { +@@ -182,7 +182,7 @@ impl Scenario { + close_after_chunks, + malformed_sse, + } => ExecutionOutcome::Success(SuccessOutcome { +- plan: build_plan_from_script( ++ plan: build_plan_from_script( + response_number, + request.model.clone(), + &request.extract_user_text(), +@@ -193,9 +193,9 @@ impl Scenario { + ), + transport: TransportOptions { + delay_before_headers_ms: delay_before_headers_ms.unwrap_or_default(), +- inter_event_delay_ms: inter_event_delay_ms.unwrap_or_default(), +- close_after_chunks: *close_after_chunks, +- malformed_sse: malformed_sse.unwrap_or(false), ++ inter_event_delay_ms: inter_event_delay_ms.unwrap_or_default(), ++ close_after_chunks: *close_after_chunks, ++ malformed_sse: malformed_sse.unwrap_or(false), + }, + }), + ScenarioScript::Error { +@@ -249,10 +249,10 @@ fn build_plan_from_script( + .into_iter() + .enumerate() + .map(|(index, tool_call)| ToolCallPlan { +- id: tool_call ++ id: tool_call + .id + .unwrap_or_else(|| format!("call_{response_number}_{index}")), +- name: tool_call.name, ++ name: tool_call.name, + arguments: tool_call.arguments, + }) + .collect(), +diff --git a/test/twin/openai/src/logs.rs b/test/twin/openai/src/logs.rs +index 1edc6cd8..01528b0d 100644 +--- a/test/twin/openai/src/logs.rs ++++ b/test/twin/openai/src/logs.rs +@@ -3,9 +3,9 @@ use serde_json::{Map, Value}; + + #[derive(Clone, Debug, Serialize)] + pub struct RequestLog { +- pub endpoint: String, +- pub model: String, +- pub stream: bool, ++ pub endpoint: String, ++ pub model: String, ++ pub stream: bool, + pub input_text: String, +- pub metadata: Map, ++ pub metadata: Map, + } +diff --git a/test/twin/openai/src/openai/models.rs b/test/twin/openai/src/openai/models.rs +index 45246ddc..92eedca7 100644 +--- a/test/twin/openai/src/openai/models.rs ++++ b/test/twin/openai/src/openai/models.rs +@@ -8,23 +8,23 @@ use serde_json::{Map, Value}; + /// via `#[serde(flatten)]` so the twin stays compatible as the API evolves. + #[derive(Clone, Debug, Deserialize)] + pub struct ResponsesRequest { +- pub model: String, ++ pub model: String, + #[serde(default)] +- pub input: ResponseInput, ++ pub input: ResponseInput, + #[serde(default)] +- pub stream: bool, ++ pub stream: bool, + #[serde(default)] +- pub metadata: Map, +- pub stop: Option, ++ pub metadata: Map, ++ pub stop: Option, + pub previous_response_id: Option, +- pub reasoning: Option, +- pub text: Option, +- pub tools: Option>, +- pub tool_choice: Option, ++ pub reasoning: Option, ++ pub text: Option, ++ pub tools: Option>, ++ pub tool_choice: Option, + /// Catch-all for fields the twin doesn't use (temperature, top_p, etc.) + #[allow(dead_code)] + #[serde(flatten)] +- extra: Map, ++ extra: Map, + } + + impl ResponsesRequest { +@@ -135,16 +135,16 @@ impl ResponseInput { + #[derive(Clone, Debug, Deserialize)] + pub struct InputItem { + #[serde(default)] +- pub role: Option, ++ pub role: Option, + #[serde(default)] +- pub content: InputContent, ++ pub content: InputContent, + #[serde(default)] + #[serde(rename = "type")] + pub item_type: Option, + #[serde(default)] +- pub output: Option, ++ pub output: Option, + #[serde(default)] +- pub call_id: Option, ++ pub call_id: Option, + } + + impl InputItem { +@@ -196,9 +196,9 @@ impl InputContent { + #[serde(deny_unknown_fields)] + pub struct ContentPart { + #[serde(rename = "type")] +- pub kind: String, ++ pub kind: String, + #[serde(default)] +- pub text: Option, ++ pub text: Option, + #[serde(default)] + pub image_url: Option, + } +@@ -329,17 +329,17 @@ pub struct TextOptions { + #[serde(deny_unknown_fields)] + pub struct TextFormat { + #[serde(rename = "type")] +- pub kind: String, ++ pub kind: String, + #[serde(default)] + pub json_schema: Option, + #[serde(default)] +- pub name: Option, ++ pub name: Option, + #[serde(default)] +- pub schema: Option, ++ pub schema: Option, + #[serde(default)] + pub description: Option, + #[serde(default)] +- pub strict: Option, ++ pub strict: Option, + } + + impl TextFormat { +@@ -362,29 +362,29 @@ pub struct ErrorEnvelope { + + #[derive(Clone, Debug, Serialize)] + pub struct ErrorBody { +- pub message: String, ++ pub message: String, + #[serde(rename = "type")] + pub error_type: String, +- pub param: Value, +- pub code: String, ++ pub param: Value, ++ pub code: String, + } + + #[derive(Clone, Debug)] + pub struct OpenAiError { + pub status: StatusCode, +- pub body: ErrorEnvelope, ++ pub body: ErrorEnvelope, + } + + impl OpenAiError { + pub fn invalid_request(param: &str, message: &str) -> Self { + Self { + status: StatusCode::BAD_REQUEST, +- body: ErrorEnvelope { ++ body: ErrorEnvelope { + error: ErrorBody { +- message: message.to_owned(), ++ message: message.to_owned(), + error_type: "invalid_request_error".to_owned(), +- param: Value::String(param.to_owned()), +- code: "invalid_request".to_owned(), ++ param: Value::String(param.to_owned()), ++ code: "invalid_request".to_owned(), + }, + }, + } +@@ -447,14 +447,14 @@ pub fn normalize_whitespace(input: &str) -> String { + #[derive(Clone, Debug, Deserialize)] + #[serde(deny_unknown_fields)] + pub struct ChatCompletionsRequest { +- pub model: String, +- pub messages: Vec, ++ pub model: String, ++ pub messages: Vec, + #[serde(default)] +- pub stream: bool, +- pub tools: Option>, +- pub tool_choice: Option, ++ pub stream: bool, ++ pub tools: Option>, ++ pub tool_choice: Option, + pub response_format: Option, +- pub stop: Option, ++ pub stop: Option, + } + + impl ChatCompletionsRequest { +@@ -532,7 +532,7 @@ impl ChatCompletionsRequest { + #[derive(Clone, Debug, Deserialize)] + #[serde(deny_unknown_fields)] + pub struct ChatMessage { +- pub role: String, ++ pub role: String, + pub content: Value, + } + +@@ -665,9 +665,9 @@ fn validate_chat_message_part(part: &Value, role: &str) -> Result<(), OpenAiErro + #[serde(deny_unknown_fields)] + pub struct ChatResponseFormat { + #[serde(rename = "type")] +- pub kind: String, ++ pub kind: String, + #[serde(default)] +- pub schema: Option, ++ pub schema: Option, + #[serde(default)] + pub json_schema: Option, + } +diff --git a/test/twin/openai/src/state.rs b/test/twin/openai/src/state.rs +index d456ab6c..b8ca1fed 100644 +--- a/test/twin/openai/src/state.rs ++++ b/test/twin/openai/src/state.rs +@@ -31,25 +31,25 @@ pub struct DebugSnapshot { + + #[derive(Clone, Debug, Serialize)] + pub struct NamespaceSnapshot { +- pub key: String, +- pub scenarios: Vec, ++ pub key: String, ++ pub scenarios: Vec, + pub request_logs: Vec, + } + + #[derive(Clone, Debug, Serialize)] + pub struct ScenarioSnapshot { +- pub endpoint: String, +- pub model: Option, +- pub stream: Option, ++ pub endpoint: String, ++ pub model: Option, ++ pub stream: Option, + pub input_contains: Option, +- pub metadata: serde_json::Map, +- pub script_kind: String, ++ pub metadata: serde_json::Map, ++ pub script_kind: String, + } + + #[derive(Clone, Debug)] + pub struct AppState { + pub config: Config, +- inner: Arc, ++ inner: Arc, + } + + #[derive(Debug)] +@@ -60,16 +60,16 @@ struct AppStateInner { + #[derive(Debug)] + struct NamespaceState { + next_response_number: u64, +- scenarios: Vec, +- request_logs: Vec, ++ scenarios: Vec, ++ request_logs: Vec, + } + + impl Default for NamespaceState { + fn default() -> Self { + Self { + next_response_number: 1, +- scenarios: Vec::new(), +- request_logs: Vec::new(), ++ scenarios: Vec::new(), ++ request_logs: Vec::new(), + } + } + } +@@ -125,11 +125,11 @@ impl AppState { + .or_default() + .request_logs + .push(RequestLog { +- endpoint: request.endpoint, +- model: request.model, +- stream: request.stream, ++ endpoint: request.endpoint, ++ model: request.model, ++ stream: request.stream, + input_text: request.input_text, +- metadata: request.metadata, ++ metadata: request.metadata, + }); + } + +@@ -156,17 +156,17 @@ impl AppState { + let mut result = Vec::new(); + for (key, ns) in namespaces.iter() { + result.push(NamespaceSnapshot { +- key: key.to_string(), +- scenarios: ns ++ key: key.to_string(), ++ scenarios: ns + .scenarios + .iter() + .map(|s| ScenarioSnapshot { +- endpoint: s.matcher.endpoint.clone(), +- model: s.matcher.model.clone(), +- stream: s.matcher.stream, ++ endpoint: s.matcher.endpoint.clone(), ++ model: s.matcher.model.clone(), ++ stream: s.matcher.stream, + input_contains: s.matcher.input_contains.clone(), +- metadata: s.matcher.metadata.clone(), +- script_kind: s.script.script_kind().to_owned(), ++ metadata: s.matcher.metadata.clone(), ++ script_kind: s.script.script_kind().to_owned(), + }) + .collect(), + request_logs: ns.request_logs.clone(), +diff --git a/test/twin/openai/tests/common/mod.rs b/test/twin/openai/tests/common/mod.rs +index af1f7c93..1437ff40 100644 +--- a/test/twin/openai/tests/common/mod.rs ++++ b/test/twin/openai/tests/common/mod.rs +@@ -15,50 +15,50 @@ use tokio::net::{TcpListener, TcpStream}; + use twin_openai::config::Config; + + pub struct TestServer { +- pub base_url: String, +- pub client: Client, +- pub auth_client: Client, ++ pub base_url: String, ++ pub client: Client, ++ pub auth_client: Client, + pub bearer_token: String, + } + + #[derive(Clone)] + pub struct ApiClient { + pub base_url: String, +- client: Client, ++ client: Client, + bearer_token: Option, + organization: Option, +- project: Option, ++ project: Option, + } + + pub struct RecordedResponse { +- pub status: fabro_http::StatusCode, ++ pub status: fabro_http::StatusCode, + pub headers: HashMap, +- pub body: Vec, ++ pub body: Vec, + } + + pub struct RawStreamResponse { +- pub status: u16, ++ pub status: u16, + pub headers: HashMap, +- pub body: Vec, ++ pub body: Vec, + } + + pub struct TimedStreamResponse { +- pub status: fabro_http::StatusCode, ++ pub status: fabro_http::StatusCode, + pub first_event_elapsed: Duration, +- pub chunks: Vec, ++ pub chunks: Vec, + } + + #[derive(Debug)] + pub struct ParsedSseTranscript { + pub blocks: Vec, + pub events: Vec, +- pub done: bool, ++ pub done: bool, + } + + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct ParsedSseEvent { + pub event: Option, +- pub data: String, ++ pub data: String, + } + + static NEXT_BEARER_TOKEN: AtomicU64 = AtomicU64::new(1); +@@ -71,7 +71,7 @@ pub async fn spawn_server() -> Result { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let addr: SocketAddr = listener.local_addr()?; + let app = twin_openai::build_app_with_config(Config { +- bind_addr: "127.0.0.1:0".parse().expect("valid addr"), ++ bind_addr: "127.0.0.1:0".parse().expect("valid addr"), + require_auth: true, + enable_admin: true, + }); +diff --git a/test/twin/openai/tests/debug_ui.rs b/test/twin/openai/tests/debug_ui.rs +index a748ecda..ec0a035b 100644 +--- a/test/twin/openai/tests/debug_ui.rs ++++ b/test/twin/openai/tests/debug_ui.rs +@@ -225,7 +225,7 @@ async fn debug_routes_not_accessible_when_admin_disabled() { + .expect("bind should succeed"); + let addr = listener.local_addr().expect("should have addr"); + let app = twin_openai::build_app_with_config(Config { +- bind_addr: "127.0.0.1:0".parse().expect("valid addr"), ++ bind_addr: "127.0.0.1:0".parse().expect("valid addr"), + require_auth: false, + enable_admin: false, + }); +diff --git a/test/twin/openai/tests/live_openai_contract.rs b/test/twin/openai/tests/live_openai_contract.rs +index ac971d9a..54ec9051 100644 +--- a/test/twin/openai/tests/live_openai_contract.rs ++++ b/test/twin/openai/tests/live_openai_contract.rs +@@ -24,7 +24,7 @@ const RESPONSES_STREAM_MILESTONES: &[&str] = &[ + + #[derive(Clone)] + struct LiveOptions { +- api: common::ApiClient, ++ api: common::ApiClient, + model: String, + } + +@@ -46,7 +46,7 @@ enum ChatStreamMilestone { + + #[derive(Clone, Debug, PartialEq, Eq)] + struct ToolCallObservation { +- name: String, ++ name: String, + arguments: Value, + } + +@@ -1526,7 +1526,7 @@ fn extract_response_tool_call(body: &Value) -> Option { + .find(|item| item.get("type").and_then(Value::as_str) == Some("function_call")) + .and_then(|tool_call| { + Some(ToolCallObservation { +- name: tool_call.get("name")?.as_str()?.to_owned(), ++ name: tool_call.get("name")?.as_str()?.to_owned(), + arguments: parse_json_object_string(tool_call.get("arguments")?.as_str()?)?, + }) + }) +@@ -1628,7 +1628,7 @@ fn extract_chat_tool_call(body: &Value) -> Option { + .iter() + .find_map(|tool_call| { + Some(ToolCallObservation { +- name: tool_call.get("function")?.get("name")?.as_str()?.to_owned(), ++ name: tool_call.get("function")?.get("name")?.as_str()?.to_owned(), + arguments: parse_json_object_string( + tool_call.get("function")?.get("arguments")?.as_str()?, + )?, +diff --git a/test/twin/openai/tests/responses_contract.rs b/test/twin/openai/tests/responses_contract.rs +index 9c2546eb..d399332b 100644 +--- a/test/twin/openai/tests/responses_contract.rs ++++ b/test/twin/openai/tests/responses_contract.rs +@@ -255,19 +255,22 @@ async fn responses_stream_emits_expected_sse_sequence() { + .collect::>(); + + assert_eq!(status, 200); +- assert_eq!(events, vec![ +- "response.created", +- "response.in_progress", +- "response.output_item.added", +- "response.output_item.done", +- "response.output_item.added", +- "response.content_part.added", +- "response.output_text.delta", +- "response.output_text.done", +- "response.content_part.done", +- "response.output_item.done", +- "response.completed", +- ]); ++ assert_eq!( ++ events, ++ vec![ ++ "response.created", ++ "response.in_progress", ++ "response.output_item.added", ++ "response.output_item.done", ++ "response.output_item.added", ++ "response.content_part.added", ++ "response.output_text.delta", ++ "response.output_text.done", ++ "response.content_part.done", ++ "response.output_item.done", ++ "response.completed", ++ ] ++ ); + assert!(!transcript.done); + assert!(joined.contains("deterministic: stream this request")); + assert!( +@@ -352,19 +355,22 @@ async fn responses_stream_emits_structured_output_events() { + .and_then(|text| serde_json::from_str::(&text).ok()) + .expect("structured stream output text"); + assert_eq!(status, 200); +- assert_eq!(events, vec![ +- "response.created", +- "response.in_progress", +- "response.output_item.added", +- "response.output_item.done", +- "response.output_item.added", +- "response.content_part.added", +- "response.output_text.delta", +- "response.output_text.done", +- "response.content_part.done", +- "response.output_item.done", +- "response.completed", +- ]); ++ assert_eq!( ++ events, ++ vec![ ++ "response.created", ++ "response.in_progress", ++ "response.output_item.added", ++ "response.output_item.done", ++ "response.output_item.added", ++ "response.content_part.added", ++ "response.output_text.delta", ++ "response.output_text.done", ++ "response.content_part.done", ++ "response.output_item.done", ++ "response.completed", ++ ] ++ ); + assert!(!transcript.done); + assert_eq!( + streamed_json["message"], +diff --git a/test/twin/openai/tests/tool_and_schema_contract.rs b/test/twin/openai/tests/tool_and_schema_contract.rs +index 4839db86..1ea00b2c 100644 +--- a/test/twin/openai/tests/tool_and_schema_contract.rs ++++ b/test/twin/openai/tests/tool_and_schema_contract.rs +@@ -378,17 +378,20 @@ async fn responses_stream_supports_tool_only_turn_without_fabricated_text() { + .collect::>(); + + assert_eq!(status, 200); +- assert_eq!(events, vec![ +- "response.created", +- "response.in_progress", +- "response.output_item.added", +- "response.output_item.done", +- "response.output_item.added", +- "response.function_call_arguments.delta", +- "response.function_call_arguments.done", +- "response.output_item.done", +- "response.completed", +- ]); ++ assert_eq!( ++ events, ++ vec![ ++ "response.created", ++ "response.in_progress", ++ "response.output_item.added", ++ "response.output_item.done", ++ "response.output_item.added", ++ "response.function_call_arguments.delta", ++ "response.function_call_arguments.done", ++ "response.output_item.done", ++ "response.completed", ++ ] ++ ); + assert!(joined.contains("\"id\":\"fc_call_weather\"")); + assert!(joined.contains("\"call_id\":\"call_weather\"")); + assert!(joined.contains("\"arguments\":\"{\\\"city\\\":\\\"Boston\\\"}\"")); diff --git a/nodes/fix_lints/status.json b/nodes/fix_lints/status.json new file mode 100644 index 000000000..a8a711b5a --- /dev/null +++ b/nodes/fix_lints/status.json @@ -0,0 +1,6 @@ +{ + "status": "success", + "notes": "Stage completed: fix_lints", + "failure_reason": null, + "timestamp": "2026-04-16T17:19:22.309643Z" +} \ No newline at end of file