diff --git a/Cargo.lock b/Cargo.lock index 543c7fa38..24e66986f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1286,6 +1286,7 @@ dependencies = [ "fabro-exe", "fabro-github", "fabro-graphviz", + "fabro-interview", "fabro-llm", "fabro-retro", "fabro-types", @@ -1504,6 +1505,19 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "fabro-interview" +version = "0.174.0" +dependencies = [ + "async-trait", + "dialoguer", + "fabro-util", + "serde", + "serde_json", + "tempfile", + "tokio", +] + [[package]] name = "fabro-linear" version = "0.174.0" @@ -1598,6 +1612,7 @@ dependencies = [ name = "fabro-slack" version = "0.174.0" dependencies = [ + "fabro-interview", "fabro-workflows", "futures-util", "reqwest 0.12.28", @@ -1721,7 +1736,6 @@ dependencies = [ "clap", "cli-table", "console 0.15.11", - "dialoguer", "dirs", "dotenvy", "fabro-agent", @@ -1731,6 +1745,7 @@ dependencies = [ "fabro-git-storage", "fabro-github", "fabro-graphviz", + "fabro-interview", "fabro-llm", "fabro-mcp", "fabro-retro", diff --git a/lib/crates/fabro-api/Cargo.toml b/lib/crates/fabro-api/Cargo.toml index cb783be8c..3c3a54478 100644 --- a/lib/crates/fabro-api/Cargo.toml +++ b/lib/crates/fabro-api/Cargo.toml @@ -11,6 +11,7 @@ doctest = false [dependencies] fabro-config = { path = "../fabro-config" } fabro-graphviz = { path = "../fabro-graphviz" } +fabro-interview = { path = "../fabro-interview" } fabro-workflows = { path = "../fabro-workflows", features = ["exedev"] } fabro-daytona = { path = "../fabro-daytona" } fabro-github = { path = "../fabro-github" } diff --git a/lib/crates/fabro-api/src/serve.rs b/lib/crates/fabro-api/src/serve.rs index 46ca64b55..d79d20772 100644 --- a/lib/crates/fabro-api/src/serve.rs +++ b/lib/crates/fabro-api/src/serve.rs @@ -14,10 +14,10 @@ use fabro_config::server::ServerConfig; use crate::jwt_auth::{AuthMode, AuthStrategy}; use crate::server::build_router; use crate::tls::ClientAuth; +use fabro_interview::Interviewer; use fabro_workflows::cli::backend::AgentApiBackend; use fabro_workflows::cli::SandboxProvider; use fabro_workflows::handler::default_registry; -use fabro_workflows::interviewer::Interviewer; #[derive(Args)] pub struct ServeArgs { diff --git a/lib/crates/fabro-api/src/server.rs b/lib/crates/fabro-api/src/server.rs index 0f3c5eb2a..fca334a7c 100644 --- a/lib/crates/fabro-api/src/server.rs +++ b/lib/crates/fabro-api/src/server.rs @@ -20,13 +20,12 @@ use fabro_agent::LocalSandbox; use crate::error::ApiError; use crate::jwt_auth::{AuthMode, AuthenticatedService, AuthenticatedUser}; +use fabro_interview::{Answer, Interviewer, QuestionType, WebInterviewer}; use fabro_workflows::checkpoint::Checkpoint; use fabro_workflows::context::Context; use fabro_workflows::engine::{RunConfig, WorkflowRunEngine}; use fabro_workflows::event::{EventEmitter, WorkflowRunEvent}; use fabro_workflows::handler::HandlerRegistry; -use fabro_workflows::interviewer::web::WebInterviewer; -use fabro_workflows::interviewer::{Answer, Interviewer, QuestionType}; pub use fabro_types::{ ApiQuestion, ApiQuestionOption, PaginatedRunList, PaginationMeta, @@ -1474,9 +1473,7 @@ mod tests { start -> exit }"#; - fn test_registry( - _interviewer: Arc, - ) -> HandlerRegistry { + fn test_registry(_interviewer: Arc) -> HandlerRegistry { let mut registry = HandlerRegistry::new(Box::new(StartHandler)); registry.register("start", Box::new(StartHandler)); registry.register("exit", Box::new(ExitHandler)); diff --git a/lib/crates/fabro-api/src/sessions.rs b/lib/crates/fabro-api/src/sessions.rs index 3d1b9d7a5..2655cc3c4 100644 --- a/lib/crates/fabro-api/src/sessions.rs +++ b/lib/crates/fabro-api/src/sessions.rs @@ -433,9 +433,7 @@ mod tests { use fabro_workflows::handler::start::StartHandler; use fabro_workflows::handler::HandlerRegistry; - fn test_registry( - _interviewer: Arc, - ) -> HandlerRegistry { + fn test_registry(_interviewer: Arc) -> HandlerRegistry { let mut registry = HandlerRegistry::new(Box::new(StartHandler)); registry.register("start", Box::new(StartHandler)); registry.register("exit", Box::new(ExitHandler)); diff --git a/lib/crates/fabro-api/tests/integration.rs b/lib/crates/fabro-api/tests/integration.rs index 37fd55e17..e9a4c7310 100644 --- a/lib/crates/fabro-api/tests/integration.rs +++ b/lib/crates/fabro-api/tests/integration.rs @@ -13,11 +13,11 @@ mod mtls_e2e { use fabro_api::server::{build_router, create_app_state}; use fabro_api::server_config::TlsConfig; use fabro_api::tls::{build_rustls_config, ClientAuth}; + use fabro_interview::Interviewer; use fabro_workflows::handler::agent::AgentHandler; use fabro_workflows::handler::exit::ExitHandler; use fabro_workflows::handler::start::StartHandler; use fabro_workflows::handler::HandlerRegistry; - use fabro_workflows::interviewer::Interviewer; use tokio::net::TcpListener; fn simple_registry(_interviewer: Arc) -> HandlerRegistry { @@ -431,12 +431,12 @@ mod server_lifecycle { use axum::body::Body; use axum::http::{Request, StatusCode}; use fabro_api::server::{build_router, create_app_state}; + use fabro_interview::Interviewer; use fabro_workflows::handler::agent::AgentHandler; use fabro_workflows::handler::exit::ExitHandler; use fabro_workflows::handler::human::HumanHandler; use fabro_workflows::handler::start::StartHandler; use fabro_workflows::handler::HandlerRegistry; - use fabro_workflows::interviewer::Interviewer; use tower::ServiceExt; fn gate_registry(interviewer: Arc) -> HandlerRegistry { @@ -627,11 +627,11 @@ mod sse_events { use axum::body::Body; use axum::http::{Request, StatusCode}; use fabro_api::server::{build_router, create_app_state}; + use fabro_interview::Interviewer; use fabro_workflows::handler::agent::AgentHandler; use fabro_workflows::handler::exit::ExitHandler; use fabro_workflows::handler::start::StartHandler; use fabro_workflows::handler::HandlerRegistry; - use fabro_workflows::interviewer::Interviewer; use http_body_util::BodyExt; use tower::ServiceExt; @@ -789,8 +789,8 @@ mod serve_dry_run { use axum::body::Body; use axum::http::{Request, StatusCode}; use fabro_api::server::{build_router, create_app_state}; + use fabro_interview::Interviewer; use fabro_workflows::handler::default_registry; - use fabro_workflows::interviewer::Interviewer; use tower::ServiceExt; const MINIMAL_DOT: &str = r#"digraph Test { diff --git a/lib/crates/fabro-api/tests/openapi_conformance.rs b/lib/crates/fabro-api/tests/openapi_conformance.rs index 43064247c..0c132f13b 100644 --- a/lib/crates/fabro-api/tests/openapi_conformance.rs +++ b/lib/crates/fabro-api/tests/openapi_conformance.rs @@ -9,12 +9,12 @@ use fabro_api::jwt_auth::AuthMode; use fabro_api::server::{build_router, create_app_state}; use fabro_api::server_config::*; use fabro_daytona::*; +use fabro_interview::Interviewer; use fabro_workflows::cli::run_config::*; use fabro_workflows::handler::exit::ExitHandler; use fabro_workflows::handler::start::StartHandler; use fabro_workflows::handler::HandlerRegistry; use fabro_workflows::hook::*; -use fabro_workflows::interviewer::Interviewer; use tower::ServiceExt; fn test_registry(_interviewer: Arc) -> HandlerRegistry { diff --git a/lib/crates/fabro-api/tests/pagination.rs b/lib/crates/fabro-api/tests/pagination.rs index b0f3f751b..4b223822b 100644 --- a/lib/crates/fabro-api/tests/pagination.rs +++ b/lib/crates/fabro-api/tests/pagination.rs @@ -6,10 +6,10 @@ use axum::body::Body; use axum::http::{Request, StatusCode}; use fabro_api::jwt_auth::AuthMode; use fabro_api::server::{build_router, create_app_state}; +use fabro_interview::Interviewer; use fabro_workflows::handler::exit::ExitHandler; use fabro_workflows::handler::start::StartHandler; use fabro_workflows::handler::HandlerRegistry; -use fabro_workflows::interviewer::Interviewer; use tower::ServiceExt; fn test_registry(_interviewer: Arc) -> HandlerRegistry { diff --git a/lib/crates/fabro-interview/Cargo.toml b/lib/crates/fabro-interview/Cargo.toml new file mode 100644 index 000000000..38b2b3d69 --- /dev/null +++ b/lib/crates/fabro-interview/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fabro-interview" +edition.workspace = true +version.workspace = true +license.workspace = true +description = "Human-in-the-loop interviewer traits and implementations" + +[lib] +doctest = false + +[dependencies] +serde.workspace = true +serde_json.workspace = true +async-trait.workspace = true +tokio.workspace = true +dialoguer.workspace = true +fabro-util = { path = "../fabro-util" } + +[dev-dependencies] +tokio = { workspace = true, features = ["test-util", "macros"] } +tempfile = "3" diff --git a/lib/crates/fabro-workflows/src/interviewer/auto_approve.rs b/lib/crates/fabro-interview/src/auto_approve.rs similarity index 96% rename from lib/crates/fabro-workflows/src/interviewer/auto_approve.rs rename to lib/crates/fabro-interview/src/auto_approve.rs index e647f36e6..feca03330 100644 --- a/lib/crates/fabro-workflows/src/interviewer/auto_approve.rs +++ b/lib/crates/fabro-interview/src/auto_approve.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use super::{Answer, AnswerValue, Interviewer, Question, QuestionType}; +use crate::{Answer, AnswerValue, Interviewer, Question, QuestionType}; /// Always approves: YES for yes/no, first option for multiple choice, "auto-approved" for freeform. pub struct AutoApproveInterviewer; @@ -29,7 +29,7 @@ impl Interviewer for AutoApproveInterviewer { #[cfg(test)] mod tests { use super::*; - use crate::interviewer::QuestionOption; + use crate::QuestionOption; #[tokio::test] async fn yes_no_returns_yes() { diff --git a/lib/crates/fabro-workflows/src/interviewer/callback.rs b/lib/crates/fabro-interview/src/callback.rs similarity index 93% rename from lib/crates/fabro-workflows/src/interviewer/callback.rs rename to lib/crates/fabro-interview/src/callback.rs index edbcb6b82..fd0424437 100644 --- a/lib/crates/fabro-workflows/src/interviewer/callback.rs +++ b/lib/crates/fabro-interview/src/callback.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use super::{Answer, Interviewer, Question}; +use crate::{Answer, Interviewer, Question}; /// Delegates question answering to a provided callback function. pub struct CallbackInterviewer { @@ -25,7 +25,7 @@ impl Interviewer for CallbackInterviewer { #[cfg(test)] mod tests { use super::*; - use crate::interviewer::{AnswerValue, QuestionType}; + use crate::{AnswerValue, QuestionType}; #[tokio::test] async fn calls_callback_with_question() { diff --git a/lib/crates/fabro-workflows/src/interviewer/console.rs b/lib/crates/fabro-interview/src/console.rs similarity index 94% rename from lib/crates/fabro-workflows/src/interviewer/console.rs rename to lib/crates/fabro-interview/src/console.rs index f114165d2..2c2e05a3d 100644 --- a/lib/crates/fabro-workflows/src/interviewer/console.rs +++ b/lib/crates/fabro-interview/src/console.rs @@ -6,7 +6,7 @@ use dialoguer::theme::ColorfulTheme; use fabro_util::terminal::Styles; use tokio::io::{AsyncBufReadExt, BufReader}; -use super::{Answer, AnswerValue, Interviewer, Question, QuestionType}; +use crate::{Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType}; /// Reads from stdin to collect answers. Displays formatted prompts per spec 6.4. pub struct ConsoleInterviewer { @@ -20,7 +20,7 @@ impl ConsoleInterviewer { } } -fn find_matching_option(response: &str, options: &[super::QuestionOption]) -> Option { +fn find_matching_option(response: &str, options: &[QuestionOption]) -> Option { let trimmed = response.trim(); // Try matching by key (case-insensitive) for opt in options { @@ -228,11 +228,11 @@ mod tests { #[test] fn find_matching_option_by_key() { let options = vec![ - super::super::QuestionOption { + crate::QuestionOption { key: "A".to_string(), label: "Approve".to_string(), }, - super::super::QuestionOption { + crate::QuestionOption { key: "R".to_string(), label: "Reject".to_string(), }, @@ -245,7 +245,7 @@ mod tests { #[test] fn find_matching_option_by_key_case_insensitive() { - let options = vec![super::super::QuestionOption { + let options = vec![crate::QuestionOption { key: "Y".to_string(), label: "Yes".to_string(), }]; @@ -256,11 +256,11 @@ mod tests { #[test] fn find_matching_option_by_index() { let options = vec![ - super::super::QuestionOption { + crate::QuestionOption { key: "A".to_string(), label: "Alpha".to_string(), }, - super::super::QuestionOption { + crate::QuestionOption { key: "B".to_string(), label: "Beta".to_string(), }, @@ -273,7 +273,7 @@ mod tests { #[test] fn find_matching_option_no_match() { - let options = vec![super::super::QuestionOption { + let options = vec![crate::QuestionOption { key: "A".to_string(), label: "Alpha".to_string(), }]; @@ -283,7 +283,7 @@ mod tests { #[test] fn find_matching_option_index_out_of_range() { - let options = vec![super::super::QuestionOption { + let options = vec![crate::QuestionOption { key: "A".to_string(), label: "Alpha".to_string(), }]; diff --git a/lib/crates/fabro-workflows/src/interviewer/mod.rs b/lib/crates/fabro-interview/src/lib.rs similarity index 95% rename from lib/crates/fabro-workflows/src/interviewer/mod.rs rename to lib/crates/fabro-interview/src/lib.rs index 961d3b88d..12ac75daa 100644 --- a/lib/crates/fabro-workflows/src/interviewer/mod.rs +++ b/lib/crates/fabro-interview/src/lib.rs @@ -1,10 +1,10 @@ -pub mod auto_approve; -pub mod callback; -pub mod console; -pub mod queue; -pub mod recording; -pub mod replay; -pub mod web; +mod auto_approve; +mod callback; +mod console; +mod queue; +mod recording; +mod replay; +mod web; use std::collections::HashMap; @@ -199,6 +199,15 @@ pub trait Interviewer: Send + Sync { } } +// Re-export all implementors at the crate root +pub use auto_approve::AutoApproveInterviewer; +pub use callback::CallbackInterviewer; +pub use console::ConsoleInterviewer; +pub use queue::QueueInterviewer; +pub use recording::RecordingInterviewer; +pub use replay::ReplayInterviewer; +pub use web::{PendingQuestion, WebInterviewer}; + #[cfg(test)] mod tests { use super::*; @@ -336,7 +345,7 @@ mod tests { #[tokio::test] async fn ask_with_timeout_no_timeout_returns_normally() { - let interviewer = crate::interviewer::auto_approve::AutoApproveInterviewer; + let interviewer = AutoApproveInterviewer; let q = Question::new("approve?", QuestionType::YesNo); let answer = ask_with_timeout(&interviewer, q).await; diff --git a/lib/crates/fabro-workflows/src/interviewer/queue.rs b/lib/crates/fabro-interview/src/queue.rs similarity index 94% rename from lib/crates/fabro-workflows/src/interviewer/queue.rs rename to lib/crates/fabro-interview/src/queue.rs index 8233240e7..df60194eb 100644 --- a/lib/crates/fabro-workflows/src/interviewer/queue.rs +++ b/lib/crates/fabro-interview/src/queue.rs @@ -3,7 +3,7 @@ use std::sync::Mutex; use async_trait::async_trait; -use super::{Answer, Interviewer, Question}; +use crate::{Answer, Interviewer, Question}; /// Reads answers from a pre-filled queue. Returns Skipped when empty. pub struct QueueInterviewer { @@ -30,7 +30,7 @@ impl Interviewer for QueueInterviewer { #[cfg(test)] mod tests { use super::*; - use crate::interviewer::{AnswerValue, QuestionType}; + use crate::{AnswerValue, QuestionType}; #[tokio::test] async fn returns_queued_answers_in_order() { diff --git a/lib/crates/fabro-workflows/src/interviewer/recording.rs b/lib/crates/fabro-interview/src/recording.rs similarity index 89% rename from lib/crates/fabro-workflows/src/interviewer/recording.rs rename to lib/crates/fabro-interview/src/recording.rs index c6338c02c..1f69bcaa0 100644 --- a/lib/crates/fabro-workflows/src/interviewer/recording.rs +++ b/lib/crates/fabro-interview/src/recording.rs @@ -3,8 +3,7 @@ use std::sync::Mutex; use async_trait::async_trait; -use super::{Answer, Interviewer, Question}; -use crate::error::FabroError; +use crate::{Answer, Interviewer, Question}; /// Wraps another interviewer and records all question-answer pairs. pub struct RecordingInterviewer { @@ -35,24 +34,26 @@ impl RecordingInterviewer { /// /// # Errors /// Returns an error if serialization fails. - pub fn to_json(&self) -> Result { + pub fn to_json(&self) -> std::io::Result { let recordings = self.recordings(); - serde_json::to_string_pretty(&recordings).map_err(|e| FabroError::Io(e.to_string())) + serde_json::to_string_pretty(&recordings) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } /// Deserializes recordings from a JSON string. /// /// # Errors /// Returns an error if deserialization fails. - pub fn from_json(json: &str) -> Result, FabroError> { - serde_json::from_str(json).map_err(|e| FabroError::Io(e.to_string())) + pub fn from_json(json: &str) -> std::io::Result> { + serde_json::from_str(json) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } /// Saves recordings to a file as JSON. /// /// # Errors /// Returns an error if serialization or file writing fails. - pub fn save_to_file(&self, path: &Path) -> Result<(), FabroError> { + pub fn save_to_file(&self, path: &Path) -> std::io::Result<()> { let json = self.to_json()?; std::fs::write(path, json)?; Ok(()) @@ -62,7 +63,7 @@ impl RecordingInterviewer { /// /// # Errors /// Returns an error if file reading or deserialization fails. - pub fn load_from_file(path: &Path) -> Result, FabroError> { + pub fn load_from_file(path: &Path) -> std::io::Result> { let json = std::fs::read_to_string(path)?; Self::from_json(&json) } @@ -83,8 +84,8 @@ impl Interviewer for RecordingInterviewer { #[cfg(test)] mod tests { use super::*; - use crate::interviewer::auto_approve::AutoApproveInterviewer; - use crate::interviewer::{AnswerValue, QuestionType}; + use crate::AutoApproveInterviewer; + use crate::{AnswerValue, QuestionType}; #[tokio::test] async fn records_question_answer_pairs() { diff --git a/lib/crates/fabro-workflows/src/interviewer/replay.rs b/lib/crates/fabro-interview/src/replay.rs similarity index 96% rename from lib/crates/fabro-workflows/src/interviewer/replay.rs rename to lib/crates/fabro-interview/src/replay.rs index 78fc426a8..d7e5ecbe8 100644 --- a/lib/crates/fabro-workflows/src/interviewer/replay.rs +++ b/lib/crates/fabro-interview/src/replay.rs @@ -2,7 +2,7 @@ use std::sync::Mutex; use async_trait::async_trait; -use super::{Answer, Interviewer, Question}; +use crate::{Answer, Interviewer, Question}; /// Replays recorded answers in sequence. When recordings are exhausted, /// returns `Answer::skipped()`. @@ -37,7 +37,7 @@ impl Interviewer for ReplayInterviewer { #[cfg(test)] mod tests { use super::*; - use crate::interviewer::{AnswerValue, QuestionType}; + use crate::{AnswerValue, QuestionType}; #[tokio::test] async fn replays_recorded_answers() { diff --git a/lib/crates/fabro-workflows/src/interviewer/web.rs b/lib/crates/fabro-interview/src/web.rs similarity index 97% rename from lib/crates/fabro-workflows/src/interviewer/web.rs rename to lib/crates/fabro-interview/src/web.rs index 65420fadc..811424f2d 100644 --- a/lib/crates/fabro-workflows/src/interviewer/web.rs +++ b/lib/crates/fabro-interview/src/web.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; use tokio::sync::oneshot; -use super::{Answer, Interviewer, Question}; +use crate::{Answer, Interviewer, Question}; /// A pending question waiting for an answer from an external source (e.g., HTTP endpoint). #[derive(Debug)] @@ -109,7 +109,7 @@ impl Interviewer for WebInterviewer { #[cfg(test)] mod tests { use super::*; - use crate::interviewer::{AnswerValue, QuestionType}; + use crate::{AnswerValue, QuestionType}; use std::sync::Arc; #[tokio::test] @@ -169,7 +169,7 @@ mod tests { q.timeout_seconds = Some(0.05); // Use ask_with_timeout from the parent module - let answer = crate::interviewer::ask_with_timeout(interviewer.as_ref(), q).await; + let answer = crate::ask_with_timeout(interviewer.as_ref(), q).await; assert_eq!(answer.value, AnswerValue::Timeout); } diff --git a/lib/crates/fabro-slack/Cargo.toml b/lib/crates/fabro-slack/Cargo.toml index d4daa25d1..7780ed663 100644 --- a/lib/crates/fabro-slack/Cargo.toml +++ b/lib/crates/fabro-slack/Cargo.toml @@ -9,6 +9,7 @@ description = "Slack Socket Mode integration for Fabro interviewer" doctest = false [dependencies] +fabro-interview = { path = "../fabro-interview" } fabro-workflows = { path = "../fabro-workflows" } futures-util.workspace = true serde.workspace = true diff --git a/lib/crates/fabro-slack/examples/slack_e2e.rs b/lib/crates/fabro-slack/examples/slack_e2e.rs index a041e7406..45c125582 100644 --- a/lib/crates/fabro-slack/examples/slack_e2e.rs +++ b/lib/crates/fabro-slack/examples/slack_e2e.rs @@ -1,13 +1,12 @@ use std::sync::Arc; +use fabro_interview::{ + Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType, WebInterviewer, +}; use fabro_slack::blocks::{answered_blocks, question_to_blocks}; use fabro_slack::client::{PostedMessage, SlackClient}; use fabro_slack::connection; use fabro_slack::threads::ThreadRegistry; -use fabro_workflows::interviewer::web::WebInterviewer; -use fabro_workflows::interviewer::{ - Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType, -}; struct TestCase { label: &'static str, diff --git a/lib/crates/fabro-slack/src/blocks.rs b/lib/crates/fabro-slack/src/blocks.rs index 1e0c060cd..ab3df0930 100644 --- a/lib/crates/fabro-slack/src/blocks.rs +++ b/lib/crates/fabro-slack/src/blocks.rs @@ -1,4 +1,4 @@ -use fabro_workflows::interviewer::{Question, QuestionType}; +use fabro_interview::{Question, QuestionType}; use serde_json::{json, Value}; fn text_block(text: &str) -> Value { @@ -95,7 +95,7 @@ pub fn question_to_blocks(question_id: &str, question: &Question) -> Vec #[cfg(test)] mod tests { use super::*; - use fabro_workflows::interviewer::QuestionOption; + use fabro_interview::QuestionOption; #[test] fn yes_no_produces_two_buttons() { diff --git a/lib/crates/fabro-slack/src/connection.rs b/lib/crates/fabro-slack/src/connection.rs index 87d336eea..0190a83e7 100644 --- a/lib/crates/fabro-slack/src/connection.rs +++ b/lib/crates/fabro-slack/src/connection.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use fabro_workflows::interviewer::web::WebInterviewer; +use fabro_interview::WebInterviewer; use futures_util::{SinkExt, StreamExt}; use tokio_tungstenite::tungstenite::Message; use tracing::{debug, error, info, warn}; @@ -187,7 +187,7 @@ pub async fn run( #[cfg(test)] mod tests { use super::*; - use fabro_workflows::interviewer::AnswerValue; + use fabro_interview::AnswerValue; fn registry() -> ThreadRegistry { ThreadRegistry::new() @@ -304,7 +304,7 @@ mod tests { let i_clone = Arc::clone(&interviewer); let handle = tokio::spawn(async move { - use fabro_workflows::interviewer::{Interviewer, Question, QuestionType}; + use fabro_interview::{Interviewer, Question, QuestionType}; let q = Question::new("approve?", QuestionType::YesNo); i_clone.ask(q).await }); diff --git a/lib/crates/fabro-slack/src/dispatch.rs b/lib/crates/fabro-slack/src/dispatch.rs index e9b4519da..1c8ce974b 100644 --- a/lib/crates/fabro-slack/src/dispatch.rs +++ b/lib/crates/fabro-slack/src/dispatch.rs @@ -1,4 +1,4 @@ -use fabro_workflows::interviewer::Answer; +use fabro_interview::Answer; use crate::interaction; use crate::socket::{classify_envelope, SocketEnvelope, SocketEventKind}; @@ -50,7 +50,7 @@ pub fn dispatch(envelope: &SocketEnvelope, thread_registry: &ThreadRegistry) -> #[cfg(test)] mod tests { use super::*; - use fabro_workflows::interviewer::AnswerValue; + use fabro_interview::AnswerValue; #[test] fn hello_produces_connected() { diff --git a/lib/crates/fabro-slack/src/interaction.rs b/lib/crates/fabro-slack/src/interaction.rs index 09898c9e1..1884fc54c 100644 --- a/lib/crates/fabro-slack/src/interaction.rs +++ b/lib/crates/fabro-slack/src/interaction.rs @@ -1,4 +1,4 @@ -use fabro_workflows::interviewer::Answer; +use fabro_interview::Answer; use serde_json::Value; /// Parses a Slack interaction payload and returns (question_id, Answer). @@ -63,7 +63,7 @@ fn extract_checkbox_selections(question_id: &str, payload: &Value) -> Answer { #[cfg(test)] mod tests { use super::*; - use fabro_workflows::interviewer::AnswerValue; + use fabro_interview::AnswerValue; #[test] fn parse_yes_button_click() { diff --git a/lib/crates/fabro-workflows/Cargo.toml b/lib/crates/fabro-workflows/Cargo.toml index 7aefac2f1..69add2c90 100644 --- a/lib/crates/fabro-workflows/Cargo.toml +++ b/lib/crates/fabro-workflows/Cargo.toml @@ -28,6 +28,7 @@ fabro-exe = { path = "../fabro-exe", optional = true } fabro-ssh = { path = "../fabro-ssh" } fabro-mcp = { path = "../fabro-mcp" } fabro-github = { path = "../fabro-github" } +fabro-interview = { path = "../fabro-interview" } fabro-util = { path = "../fabro-util" } fabro-daytona = { path = "../fabro-daytona" } fabro-git-storage = { path = "../fabro-git-storage" } @@ -45,7 +46,6 @@ futures.workspace = true chrono = { workspace = true, features = ["serde"] } toml.workspace = true dirs = "6" -dialoguer.workspace = true regex.workspace = true scopeguard = "1" hex.workspace = true diff --git a/lib/crates/fabro-workflows/src/cli/progress.rs b/lib/crates/fabro-workflows/src/cli/progress.rs index 833856646..450a0e14c 100644 --- a/lib/crates/fabro-workflows/src/cli/progress.rs +++ b/lib/crates/fabro-workflows/src/cli/progress.rs @@ -8,9 +8,9 @@ use console::Style; use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; use crate::event::{EventEmitter, WorkflowRunEvent}; -use crate::interviewer::{Answer, Interviewer, Question}; use crate::outcome::StageStatus; use fabro_agent::AgentEvent; +use fabro_interview::{Answer, ConsoleInterviewer, Interviewer, Question}; use super::{compute_stage_cost, format_cost, format_tokens_human}; @@ -1266,15 +1266,12 @@ impl ProgressUI { /// Wraps a `ConsoleInterviewer` so that progress bars are hidden during /// interactive prompts (avoids garbled output from concurrent writes). pub struct ProgressAwareInterviewer { - inner: crate::interviewer::console::ConsoleInterviewer, + inner: ConsoleInterviewer, progress: Arc>, } impl ProgressAwareInterviewer { - pub fn new( - inner: crate::interviewer::console::ConsoleInterviewer, - progress: Arc>, - ) -> Self { + pub fn new(inner: ConsoleInterviewer, progress: Arc>) -> Self { Self { inner, progress } } diff --git a/lib/crates/fabro-workflows/src/cli/run.rs b/lib/crates/fabro-workflows/src/cli/run.rs index f9035774e..8c06feea3 100644 --- a/lib/crates/fabro-workflows/src/cli/run.rs +++ b/lib/crates/fabro-workflows/src/cli/run.rs @@ -15,11 +15,9 @@ use crate::checkpoint::Checkpoint; use crate::engine::{RunConfig, WorkflowRunEngine}; use crate::event::EventEmitter; use crate::handler::default_registry; -use crate::interviewer::auto_approve::AutoApproveInterviewer; -use crate::interviewer::console::ConsoleInterviewer; -use crate::interviewer::Interviewer; use crate::outcome::StageStatus; use crate::workflow::WorkflowBuilder; +use fabro_interview::{AutoApproveInterviewer, ConsoleInterviewer, Interviewer}; use fabro_validate::Severity; use fabro_llm::provider::Provider; @@ -1708,10 +1706,10 @@ async fn run_from_branch( }; // Build interviewer - let interviewer: Arc = if args.auto_approve { - Arc::new(crate::interviewer::auto_approve::AutoApproveInterviewer) + let interviewer: Arc = if args.auto_approve { + Arc::new(fabro_interview::AutoApproveInterviewer) } else { - Arc::new(crate::interviewer::console::ConsoleInterviewer::new(styles)) + Arc::new(fabro_interview::ConsoleInterviewer::new(styles)) }; // Build engine with a backend diff --git a/lib/crates/fabro-workflows/src/engine.rs b/lib/crates/fabro-workflows/src/engine.rs index 077481a61..901f68423 100644 --- a/lib/crates/fabro-workflows/src/engine.rs +++ b/lib/crates/fabro-workflows/src/engine.rs @@ -24,11 +24,11 @@ use crate::error::{FabroError, FailureClass, FailureSignature, Result}; use crate::event::{EventEmitter, WorkflowRunEvent}; use crate::handler::{EngineServices, HandlerRegistry}; use crate::hook::{HookContext, HookDecision, HookEvent, HookRunner}; -use crate::interviewer::Interviewer; use crate::millis_u64; use crate::outcome::{Outcome, StageStatus}; use crate::preamble::build_preamble; use fabro_graphviz::graph::{Edge, Graph, Node}; +use fabro_interview::Interviewer; /// Classify the failure mode of a completed outcome. /// diff --git a/lib/crates/fabro-workflows/src/handler/human.rs b/lib/crates/fabro-workflows/src/handler/human.rs index 619565d38..27aec49e8 100644 --- a/lib/crates/fabro-workflows/src/handler/human.rs +++ b/lib/crates/fabro-workflows/src/handler/human.rs @@ -8,12 +8,10 @@ use crate::context::keys; use crate::context::Context; use crate::error::FabroError; use crate::event::{EventEmitter, WorkflowRunEvent}; -use crate::interviewer::{ - Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType, -}; use crate::millis_u64; use crate::outcome::Outcome; use fabro_graphviz::graph::{Graph, Node}; +use fabro_interview::{Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType}; use super::{EngineServices, Handler}; @@ -327,9 +325,8 @@ fn answer_text(answer: &Answer) -> String { #[cfg(test)] mod tests { use super::*; - use crate::interviewer::auto_approve::AutoApproveInterviewer; - use crate::interviewer::recording::RecordingInterviewer; use fabro_graphviz::graph::{AttrValue, Edge}; + use fabro_interview::{AutoApproveInterviewer, RecordingInterviewer}; fn make_services() -> EngineServices { EngineServices::test_default() @@ -437,9 +434,9 @@ mod tests { #[tokio::test] async fn wait_human_with_freeform_edge() { - let interviewer = Arc::new(crate::interviewer::callback::CallbackInterviewer::new( - |_| Answer::text("custom input"), - )); + let interviewer = Arc::new(fabro_interview::CallbackInterviewer::new(|_| { + Answer::text("custom input") + })); let handler = HumanHandler::new(interviewer); let mut graph = Graph::new("test"); @@ -474,9 +471,9 @@ mod tests { #[tokio::test] async fn freeform_only_gate_uses_freeform_question_type() { - let inner = Box::new(crate::interviewer::callback::CallbackInterviewer::new( - |_| Answer::text("hello"), - )); + let inner = Box::new(fabro_interview::CallbackInterviewer::new(|_| { + Answer::text("hello") + })); let recorder = Arc::new(RecordingInterviewer::new(inner)); let handler = HumanHandler::new(recorder.clone()); diff --git a/lib/crates/fabro-workflows/src/handler/mod.rs b/lib/crates/fabro-workflows/src/handler/mod.rs index db4552935..5d05b7b3d 100644 --- a/lib/crates/fabro-workflows/src/handler/mod.rs +++ b/lib/crates/fabro-workflows/src/handler/mod.rs @@ -22,9 +22,9 @@ use crate::engine::GitState; use crate::error::FabroError; use crate::event::EventEmitter; use crate::hook::{HookContext, HookDecision, HookRunner}; -use crate::interviewer::Interviewer; use crate::outcome::Outcome; use fabro_graphviz::graph::{shape_to_handler_type, Graph, Node}; +use fabro_interview::Interviewer; /// Shared services available to all handlers during execution. pub struct EngineServices { diff --git a/lib/crates/fabro-workflows/src/lib.rs b/lib/crates/fabro-workflows/src/lib.rs index 58bc2345a..af924ce2c 100644 --- a/lib/crates/fabro-workflows/src/lib.rs +++ b/lib/crates/fabro-workflows/src/lib.rs @@ -107,7 +107,6 @@ pub mod event; pub mod git; pub mod handler; pub mod hook; -pub mod interviewer; pub mod manifest; pub mod outcome; pub mod preamble; diff --git a/lib/crates/fabro-workflows/tests/integration.rs b/lib/crates/fabro-workflows/tests/integration.rs index a54866ddb..894997e94 100644 --- a/lib/crates/fabro-workflows/tests/integration.rs +++ b/lib/crates/fabro-workflows/tests/integration.rs @@ -5,6 +5,10 @@ use std::time::Duration; use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node}; use fabro_graphviz::parser::parse; +use fabro_interview::{ + Answer, AnswerValue, AutoApproveInterviewer, Interviewer, QueueInterviewer, + RecordingInterviewer, +}; use fabro_llm::provider::Provider; use fabro_validate::{validate, validate_or_raise, Severity}; use fabro_workflows::checkpoint::Checkpoint; @@ -23,10 +27,6 @@ use fabro_workflows::handler::manager_loop::SubWorkflowHandler; use fabro_workflows::handler::start::StartHandler; use fabro_workflows::handler::wait::WaitHandler; use fabro_workflows::handler::{Handler, HandlerRegistry}; -use fabro_workflows::interviewer::auto_approve::AutoApproveInterviewer; -use fabro_workflows::interviewer::queue::QueueInterviewer; -use fabro_workflows::interviewer::recording::RecordingInterviewer; -use fabro_workflows::interviewer::{Answer, AnswerValue, Interviewer}; use fabro_workflows::outcome::{Outcome, StageStatus}; use fabro_workflows::stylesheet::{apply_stylesheet, parse_stylesheet}; use fabro_workflows::transform::{ @@ -6106,6 +6106,7 @@ mod real_llm { use super::local_env; use fabro_graphviz::graph::{AttrValue, Edge, Graph}; + use fabro_interview::AutoApproveInterviewer; use fabro_workflows::checkpoint::Checkpoint; use fabro_workflows::engine::{RunConfig, WorkflowRunEngine}; use fabro_workflows::event::EventEmitter; @@ -6113,7 +6114,6 @@ mod real_llm { use fabro_workflows::handler::human::HumanHandler; use fabro_workflows::handler::start::StartHandler; use fabro_workflows::handler::HandlerRegistry; - use fabro_workflows::interviewer::auto_approve::AutoApproveInterviewer; use fabro_workflows::outcome::StageStatus; #[tokio::test]