Extract fabro-interview crate from fabro-workflows

The interviewer module (trait + 7 implementations for human-in-the-loop
interactions) had zero dependencies on fabro-workflows internals, making
it a clean extraction. Consumers (fabro-api, fabro-slack) now depend on
fabro-interview directly instead of reaching through fabro-workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-17 13:53:36 -04:00
parent 4da08d269b
commit c18062bc3f
No known key found for this signature in database
31 changed files with 132 additions and 99 deletions

17
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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 {

View file

@ -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<dyn fabro_workflows::interviewer::Interviewer>,
) -> HandlerRegistry {
fn test_registry(_interviewer: Arc<dyn fabro_interview::Interviewer>) -> HandlerRegistry {
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
registry.register("start", Box::new(StartHandler));
registry.register("exit", Box::new(ExitHandler));

View file

@ -433,9 +433,7 @@ mod tests {
use fabro_workflows::handler::start::StartHandler;
use fabro_workflows::handler::HandlerRegistry;
fn test_registry(
_interviewer: Arc<dyn fabro_workflows::interviewer::Interviewer>,
) -> HandlerRegistry {
fn test_registry(_interviewer: Arc<dyn fabro_interview::Interviewer>) -> HandlerRegistry {
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
registry.register("start", Box::new(StartHandler));
registry.register("exit", Box::new(ExitHandler));

View file

@ -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<dyn Interviewer>) -> 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<dyn Interviewer>) -> 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 {

View file

@ -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<dyn Interviewer>) -> HandlerRegistry {

View file

@ -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<dyn Interviewer>) -> HandlerRegistry {

View file

@ -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"

View file

@ -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() {

View file

@ -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() {

View file

@ -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<Answer> {
fn find_matching_option(response: &str, options: &[QuestionOption]) -> Option<Answer> {
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(),
}];

View file

@ -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;

View file

@ -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() {

View file

@ -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<String, FabroError> {
pub fn to_json(&self) -> std::io::Result<String> {
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<Vec<(Question, Answer)>, FabroError> {
serde_json::from_str(json).map_err(|e| FabroError::Io(e.to_string()))
pub fn from_json(json: &str) -> std::io::Result<Vec<(Question, Answer)>> {
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<Vec<(Question, Answer)>, FabroError> {
pub fn load_from_file(path: &Path) -> std::io::Result<Vec<(Question, Answer)>> {
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() {

View file

@ -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() {

View file

@ -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);
}

View file

@ -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

View file

@ -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,

View file

@ -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<Value>
#[cfg(test)]
mod tests {
use super::*;
use fabro_workflows::interviewer::QuestionOption;
use fabro_interview::QuestionOption;
#[test]
fn yes_no_produces_two_buttons() {

View file

@ -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
});

View file

@ -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() {

View file

@ -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() {

View file

@ -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

View file

@ -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<Mutex<ProgressUI>>,
}
impl ProgressAwareInterviewer {
pub fn new(
inner: crate::interviewer::console::ConsoleInterviewer,
progress: Arc<Mutex<ProgressUI>>,
) -> Self {
pub fn new(inner: ConsoleInterviewer, progress: Arc<Mutex<ProgressUI>>) -> Self {
Self { inner, progress }
}

View file

@ -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<dyn crate::interviewer::Interviewer> = if args.auto_approve {
Arc::new(crate::interviewer::auto_approve::AutoApproveInterviewer)
let interviewer: Arc<dyn fabro_interview::Interviewer> = 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

View file

@ -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.
///

View file

@ -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());

View file

@ -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 {

View file

@ -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;

View file

@ -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]