mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
Refactor workflow runtime initialization
This commit is contained in:
parent
b748c7e6ea
commit
205d40d687
28 changed files with 1532 additions and 1290 deletions
|
|
@ -37,7 +37,7 @@ Every line in `progress.jsonl` has three envelope fields, then the event's own f
|
|||
| `run_id` | string | ULID for this workflow run |
|
||||
| `event` | string | Event name (matches Rust variant, dot-separated for wrapped types) |
|
||||
|
||||
The envelope is built in `cli/run.rs`. Field names from the event that collide with envelope keys (`ts`, `run_id`, `event`) are dropped — the `run_id` from `WorkflowRunStarted` populates the envelope itself.
|
||||
The envelope is built in `fabro-workflows/src/event.rs` by `build_event_envelope()`, and file logging is handled by `ProgressLogger`. Field names from the event that collide with envelope keys (`ts`, `run_id`, `event`) are dropped — the `run_id` from `WorkflowRunStarted` populates the envelope itself.
|
||||
|
||||
## Run Completion Contract
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ WorkflowRunEvent::MyNewEvent { node_id, duration_ms, .. } => {
|
|||
|
||||
| Event | JSONL fields |
|
||||
|---|---|
|
||||
| `WorkflowRunStarted` | `workflow_name`, `run_id`, `base_sha`?, `run_branch`?, `worktree_dir`? |
|
||||
| `WorkflowRunStarted` | `workflow_name`, `run_id`, `base_branch`?, `base_sha`?, `run_branch`?, `worktree_dir`? |
|
||||
| `WorkflowRunCompleted` | `duration_ms`, `artifact_count`, `total_cost`?, `final_git_commit_sha`? |
|
||||
| `WorkflowRunFailed` | `error`, `duration_ms`, `git_commit_sha`? |
|
||||
| `RunNotice` | `level`, `code`, `message` |
|
||||
|
|
|
|||
|
|
@ -14,10 +14,8 @@ use fabro_config::FabroConfig;
|
|||
use crate::jwt_auth::{AuthMode, AuthStrategy};
|
||||
use crate::server::build_router;
|
||||
use crate::tls::ClientAuth;
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_workflows::handler::default_registry;
|
||||
use fabro_workflows::handler::llm::AgentApiBackend;
|
||||
use fabro_workflows::pipeline::LlmSpec;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ServeArgs {
|
||||
|
|
@ -97,23 +95,19 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
|
||||
// Build registry factory that reads live config
|
||||
let config_for_factory = Arc::clone(&shared_config);
|
||||
let factory = move |interviewer: Arc<dyn Interviewer>| {
|
||||
let factory = move || {
|
||||
let (model, provider_enum) = resolve_model_provider(
|
||||
&config_for_factory,
|
||||
cli_model.as_deref(),
|
||||
cli_provider.as_deref(),
|
||||
);
|
||||
default_registry(interviewer, move || {
|
||||
if dry_run_mode {
|
||||
None
|
||||
} else {
|
||||
Some(Box::new(AgentApiBackend::new(
|
||||
model.clone(),
|
||||
provider_enum,
|
||||
Vec::new(),
|
||||
)))
|
||||
}
|
||||
})
|
||||
LlmSpec {
|
||||
model,
|
||||
provider: provider_enum,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: dry_run_mode,
|
||||
}
|
||||
};
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
let db = fabro_db::connect(&data_dir.join("fabro.db")).await?;
|
||||
|
|
|
|||
|
|
@ -16,16 +16,15 @@ use tower::ServiceExt;
|
|||
|
||||
use tracing::{error, info};
|
||||
|
||||
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::context::Context;
|
||||
use fabro_workflows::event::{EventEmitter, WorkflowRunEvent};
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::operations::{self, RunCreateOptions};
|
||||
use fabro_workflows::pipeline::{self, InitOptions, Persisted};
|
||||
use fabro_workflows::pipeline::{
|
||||
self, InitOptions, LlmSpec, Persisted, SandboxEnvSpec, SandboxSpec,
|
||||
};
|
||||
use fabro_workflows::records::Checkpoint;
|
||||
use fabro_workflows::run_options::LifecycleOptions;
|
||||
use fabro_workflows::run_options::RunOptions;
|
||||
|
|
@ -101,7 +100,7 @@ struct AggregateUsageTotals {
|
|||
pub struct AppState {
|
||||
runs: Mutex<HashMap<String, ManagedRun>>,
|
||||
aggregate_usage: Mutex<AggregateUsageTotals>,
|
||||
registry_factory: Box<dyn Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync>,
|
||||
llm_spec_factory: Box<dyn Fn() -> LlmSpec + Send + Sync>,
|
||||
pub dry_run: bool,
|
||||
pub db: sqlx::SqlitePool,
|
||||
max_concurrent_runs: usize,
|
||||
|
|
@ -381,17 +380,14 @@ async fn get_aggregate_usage(
|
|||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
|
||||
/// Create an `AppState` with the given registry factory and database pool.
|
||||
///
|
||||
/// The factory receives the run's `WebInterviewer` so it can wire it
|
||||
/// into handlers that need human-in-the-loop interaction (e.g., `HumanHandler`).
|
||||
/// Create an `AppState` with the given LLM spec factory and database pool.
|
||||
pub fn create_app_state(
|
||||
db: sqlx::SqlitePool,
|
||||
registry_factory: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
|
||||
llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static,
|
||||
) -> Arc<AppState> {
|
||||
create_app_state_with_options(
|
||||
db,
|
||||
registry_factory,
|
||||
llm_spec_factory,
|
||||
false,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
|
|
@ -399,10 +395,10 @@ pub fn create_app_state(
|
|||
)
|
||||
}
|
||||
|
||||
/// Create an `AppState` with the given database pool, registry factory, dry-run flag, and concurrency limit.
|
||||
/// Create an `AppState` with the given database pool, LLM spec factory, dry-run flag, and concurrency limit.
|
||||
pub fn create_app_state_with_options(
|
||||
db: sqlx::SqlitePool,
|
||||
registry_factory: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
|
||||
llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static,
|
||||
dry_run: bool,
|
||||
max_concurrent_runs: usize,
|
||||
git_author: fabro_workflows::git::GitAuthor,
|
||||
|
|
@ -411,7 +407,7 @@ pub fn create_app_state_with_options(
|
|||
Arc::new(AppState {
|
||||
runs: Mutex::new(HashMap::new()),
|
||||
aggregate_usage: Mutex::new(AggregateUsageTotals::default()),
|
||||
registry_factory: Box::new(registry_factory),
|
||||
llm_spec_factory: Box::new(llm_spec_factory),
|
||||
dry_run,
|
||||
db,
|
||||
max_concurrent_runs,
|
||||
|
|
@ -616,11 +612,11 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
|
|||
None => return,
|
||||
};
|
||||
|
||||
let registry = (state.registry_factory)(Arc::clone(&interviewer) as Arc<dyn Interviewer>);
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
let sandbox: Arc<dyn fabro_agent::Sandbox> = Arc::new(
|
||||
fabro_agent::ReadBeforeWriteSandbox::new(Arc::new(LocalSandbox::new(cwd))),
|
||||
);
|
||||
let sandbox = SandboxSpec::Local {
|
||||
working_directory: cwd,
|
||||
};
|
||||
let llm = (state.llm_spec_factory)();
|
||||
let emitter = Arc::new(emitter);
|
||||
|
||||
// Transition to Running, populate interviewer + context
|
||||
|
|
@ -664,14 +660,14 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
|
|||
workflow_slug: run_record.workflow_slug,
|
||||
github_app: None,
|
||||
base_branch: run_record.base_branch,
|
||||
display_base_sha: None,
|
||||
host_repo_path: run_record.host_repo_path.map(Into::into),
|
||||
git: None,
|
||||
};
|
||||
|
||||
let execution = {
|
||||
let emitter = Arc::clone(&emitter);
|
||||
let sandbox = Arc::clone(&sandbox);
|
||||
let registry = Arc::new(registry);
|
||||
let interviewer = Arc::clone(&interviewer) as Arc<dyn Interviewer>;
|
||||
let run_id = run_id.clone();
|
||||
let run_options = run_options.clone();
|
||||
let hooks = state.hooks.clone();
|
||||
|
|
@ -684,7 +680,8 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
|
|||
dry_run,
|
||||
emitter,
|
||||
sandbox,
|
||||
registry,
|
||||
llm,
|
||||
interviewer,
|
||||
lifecycle: LifecycleOptions {
|
||||
setup_commands: Vec::new(),
|
||||
setup_command_timeout_ms: 300_000,
|
||||
|
|
@ -692,7 +689,15 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
|
|||
},
|
||||
run_options,
|
||||
hooks: fabro_hooks::HookConfig { hooks },
|
||||
sandbox_env: HashMap::new(),
|
||||
sandbox_env: SandboxEnvSpec {
|
||||
devcontainer_env: HashMap::new(),
|
||||
toml_env: HashMap::new(),
|
||||
github_permissions: None,
|
||||
origin_url: None,
|
||||
},
|
||||
devcontainer: None,
|
||||
git: None,
|
||||
worktree_mode: None,
|
||||
checkpoint: None,
|
||||
seed_context: None,
|
||||
},
|
||||
|
|
@ -1540,9 +1545,6 @@ mod tests {
|
|||
use axum::http::Request;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use fabro_workflows::handler::exit::ExitHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
|
||||
const MINIMAL_DOT: &str = r#"digraph Test {
|
||||
graph [goal="Test"]
|
||||
start [shape=Mdiamond]
|
||||
|
|
@ -1550,11 +1552,14 @@ mod tests {
|
|||
start -> exit
|
||||
}"#;
|
||||
|
||||
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));
|
||||
registry
|
||||
fn test_llm_spec() -> LlmSpec {
|
||||
LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_db() -> sqlx::SqlitePool {
|
||||
|
|
@ -1564,7 +1569,7 @@ mod tests {
|
|||
}
|
||||
|
||||
fn test_app_with(db: sqlx::SqlitePool) -> Router {
|
||||
let state = create_app_state(db, test_registry);
|
||||
let state = create_app_state(db, test_llm_spec);
|
||||
build_router(state, AuthMode::Disabled)
|
||||
}
|
||||
|
||||
|
|
@ -1616,7 +1621,7 @@ mod tests {
|
|||
async fn test_model_dry_run_returns_ok() {
|
||||
let state = create_app_state_with_options(
|
||||
test_db().await,
|
||||
test_registry,
|
||||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
|
|
@ -1643,7 +1648,7 @@ mod tests {
|
|||
async fn test_model_dry_run_unknown_returns_404() {
|
||||
let state = create_app_state_with_options(
|
||||
test_db().await,
|
||||
test_registry,
|
||||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
|
|
@ -1702,7 +1707,7 @@ mod tests {
|
|||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_run_status_returns_status() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = test_app_with_scheduler(state);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -1760,7 +1765,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn get_questions_returns_empty_list() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -1825,7 +1830,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn get_checkpoint_returns_null_initially() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -1855,7 +1860,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn get_context_returns_map() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -1888,7 +1893,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn cancel_run_succeeds() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -1937,7 +1942,7 @@ mod tests {
|
|||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_events_returns_sse_stream() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = test_app_with_scheduler(state);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -1988,7 +1993,7 @@ mod tests {
|
|||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn run_completes_and_status_is_completed() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = test_app_with_scheduler(state);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -2027,7 +2032,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn get_graph_returns_svg() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -2095,7 +2100,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn list_runs_returns_started_run() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
// List should be empty initially
|
||||
|
|
@ -2144,7 +2149,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn get_aggregate_usage_returns_zeros_initially() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
let req = Request::builder()
|
||||
|
|
@ -2167,7 +2172,7 @@ mod tests {
|
|||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn aggregate_usage_increments_after_run_completes() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = test_app_with_scheduler(state);
|
||||
|
||||
// Start a run
|
||||
|
|
@ -2218,7 +2223,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn post_runs_returns_queued_status() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
||||
let req = Request::builder()
|
||||
|
|
@ -2249,7 +2254,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn cancel_queued_run_succeeds() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
||||
// Submit a run (no scheduler, stays queued)
|
||||
|
|
@ -2290,7 +2295,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn queue_position_reported_for_queued_runs() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
||||
// Submit two runs (no scheduler, both stay queued)
|
||||
|
|
@ -2334,7 +2339,7 @@ mod tests {
|
|||
async fn concurrency_limit_respected() {
|
||||
let state = create_app_state_with_options(
|
||||
test_db().await,
|
||||
test_registry,
|
||||
test_llm_spec,
|
||||
false,
|
||||
1,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
|
|
@ -2388,7 +2393,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn submit_answer_to_queued_run_returns_conflict() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
||||
let req = Request::builder()
|
||||
|
|
@ -2422,7 +2427,7 @@ mod tests {
|
|||
async fn create_completion_non_streaming_returns_json() {
|
||||
let state = create_app_state_with_options(
|
||||
test_db().await,
|
||||
test_registry,
|
||||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
|
|
@ -2459,7 +2464,7 @@ mod tests {
|
|||
async fn create_completion_streaming_returns_sse() {
|
||||
let state = create_app_state_with_options(
|
||||
test_db().await,
|
||||
test_registry,
|
||||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
|
|
|
|||
|
|
@ -430,15 +430,16 @@ mod tests {
|
|||
use crate::jwt_auth::AuthMode;
|
||||
use crate::server::{build_router, create_app_state_with_options};
|
||||
|
||||
use fabro_workflows::handler::exit::ExitHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::pipeline::LlmSpec;
|
||||
|
||||
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));
|
||||
registry
|
||||
fn test_llm_spec() -> LlmSpec {
|
||||
LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_db() -> sqlx::SqlitePool {
|
||||
|
|
@ -451,7 +452,7 @@ mod tests {
|
|||
let db = test_db().await;
|
||||
let state = create_app_state_with_options(
|
||||
db,
|
||||
test_registry,
|
||||
test_llm_spec,
|
||||
true,
|
||||
5,
|
||||
fabro_workflows::git::GitAuthor::default(),
|
||||
|
|
|
|||
|
|
@ -7,25 +7,22 @@
|
|||
mod mtls_e2e {
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_api::jwt_auth::{AuthMode, AuthStrategy};
|
||||
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::pipeline::LlmSpec;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
fn simple_registry(_interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
let mut registry = HandlerRegistry::new(Box::new(AgentHandler::new(None)));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
registry.register("agent", Box::new(AgentHandler::new(None)));
|
||||
registry
|
||||
fn test_llm_spec() -> LlmSpec {
|
||||
LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_db() -> sqlx::SqlitePool {
|
||||
|
|
@ -194,7 +191,7 @@ mod mtls_e2e {
|
|||
let rustls_config = build_rustls_config(tls_config, client_auth);
|
||||
let tls_acceptor = tokio_rustls::TlsAcceptor::from(rustls_config);
|
||||
|
||||
let state = create_app_state(test_db().await, simple_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let router = build_router(state, auth_mode);
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
|
@ -431,21 +428,17 @@ 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::pipeline::LlmSpec;
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn gate_registry(interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
let mut registry = HandlerRegistry::new(Box::new(AgentHandler::new(None)));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
registry.register("agent", Box::new(AgentHandler::new(None)));
|
||||
registry.register("human", Box::new(HumanHandler::new(interviewer)));
|
||||
registry
|
||||
fn test_llm_spec() -> LlmSpec {
|
||||
LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn body_json(body: Body) -> serde_json::Value {
|
||||
|
|
@ -477,7 +470,7 @@ mod server_lifecycle {
|
|||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn full_http_lifecycle_approve_and_complete() {
|
||||
let state = create_app_state(test_db().await, gate_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
fabro_api::server::spawn_scheduler(Arc::clone(&state));
|
||||
let app = build_router(Arc::clone(&state), fabro_api::jwt_auth::AuthMode::Disabled);
|
||||
|
||||
|
|
@ -573,7 +566,7 @@ mod server_lifecycle {
|
|||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn full_http_lifecycle_cancel() {
|
||||
let state = create_app_state(test_db().await, gate_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
fabro_api::server::spawn_scheduler(Arc::clone(&state));
|
||||
let app = build_router(Arc::clone(&state), fabro_api::jwt_auth::AuthMode::Disabled);
|
||||
|
||||
|
|
@ -627,20 +620,18 @@ 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::pipeline::LlmSpec;
|
||||
use http_body_util::BodyExt;
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn simple_registry(_interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
let mut registry = HandlerRegistry::new(Box::new(AgentHandler::new(None)));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
registry.register("agent", Box::new(AgentHandler::new(None)));
|
||||
registry
|
||||
fn test_llm_spec() -> LlmSpec {
|
||||
LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
|
||||
const SIMPLE_DOT: &str = r#"digraph SSETest {
|
||||
|
|
@ -659,7 +650,7 @@ mod sse_events {
|
|||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn sse_stream_contains_expected_event_types() {
|
||||
let state = create_app_state(test_db().await, simple_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
fabro_api::server::spawn_scheduler(Arc::clone(&state));
|
||||
let app = build_router(Arc::clone(&state), fabro_api::jwt_auth::AuthMode::Disabled);
|
||||
|
||||
|
|
@ -789,8 +780,7 @@ 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::pipeline::LlmSpec;
|
||||
use tower::ServiceExt;
|
||||
|
||||
const MINIMAL_DOT: &str = r#"digraph Test {
|
||||
|
|
@ -806,10 +796,19 @@ mod serve_dry_run {
|
|||
pool
|
||||
}
|
||||
|
||||
fn test_llm_spec() -> LlmSpec {
|
||||
LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the router exactly as `serve_command` does in dry-run mode.
|
||||
async fn dry_run_app() -> axum::Router {
|
||||
let factory = |interviewer: Arc<dyn Interviewer>| default_registry(interviewer, || None);
|
||||
let state = create_app_state(test_db().await, factory);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
fabro_api::server::spawn_scheduler(Arc::clone(&state));
|
||||
build_router(state, fabro_api::jwt_auth::AuthMode::Disabled)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
//! Conformance tests: spec ↔ router ↔ Rust struct consistency.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{Method, Request, StatusCode};
|
||||
|
|
@ -11,18 +10,18 @@ use fabro_api::server_config::*;
|
|||
use fabro_config::run::*;
|
||||
use fabro_config::sandbox::SandboxConfig;
|
||||
use fabro_hooks::*;
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_sandbox::daytona::*;
|
||||
use fabro_workflows::handler::exit::ExitHandler;
|
||||
use fabro_workflows::handler::start::StartHandler;
|
||||
use fabro_workflows::handler::HandlerRegistry;
|
||||
use fabro_workflows::pipeline::LlmSpec;
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn test_registry(_interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
registry
|
||||
fn test_llm_spec() -> LlmSpec {
|
||||
LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_db() -> sqlx::SqlitePool {
|
||||
|
|
@ -75,7 +74,7 @@ fn methods_for_path_item(item: &openapiv3::PathItem) -> Vec<Method> {
|
|||
#[tokio::test]
|
||||
async fn all_spec_routes_are_routable() {
|
||||
let spec = load_spec();
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
||||
let mut checked = 0;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
//! Tests that paginated list endpoints return `{ data, meta: { has_more } }`.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
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::pipeline::LlmSpec;
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn test_registry(_interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
|
||||
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
registry
|
||||
fn test_llm_spec() -> LlmSpec {
|
||||
LlmSpec {
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
fallback_chain: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_db() -> sqlx::SqlitePool {
|
||||
|
|
@ -109,7 +107,7 @@ const ENDPOINTS: &[PaginatedEndpoint] = &[
|
|||
|
||||
#[tokio::test]
|
||||
async fn paginated_endpoints_return_correct_shape() {
|
||||
let state = create_app_state(test_db().await, test_registry);
|
||||
let state = create_app_state(test_db().await, test_llm_spec);
|
||||
let app = build_router(state, AuthMode::Disabled);
|
||||
|
||||
for ep in ENDPOINTS {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use chrono::Utc;
|
||||
use fabro_workflows::event::{RunNoticeLevel, WorkflowRunEvent};
|
||||
use fabro_workflows::outcome::StageStatus;
|
||||
use fabro_workflows::records::Conclusion;
|
||||
|
|
@ -111,52 +110,12 @@ pub(crate) fn load_run_id(run_dir: &Path) -> Option<String> {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_event_envelope(event: &WorkflowRunEvent, run_id: &str) -> serde_json::Value {
|
||||
let (event_name, event_fields) = fabro_workflows::event::flatten_event(event);
|
||||
let mut envelope = serde_json::Map::new();
|
||||
envelope.insert(
|
||||
"ts".to_string(),
|
||||
serde_json::Value::String(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)),
|
||||
);
|
||||
envelope.insert(
|
||||
"run_id".to_string(),
|
||||
serde_json::Value::String(run_id.to_string()),
|
||||
);
|
||||
envelope.insert("event".to_string(), serde_json::Value::String(event_name));
|
||||
for (k, v) in event_fields {
|
||||
if k != "ts" && k != "run_id" && k != "event" {
|
||||
envelope.insert(k, v);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(envelope)
|
||||
}
|
||||
|
||||
pub(crate) fn append_progress_event(
|
||||
run_dir: &Path,
|
||||
run_id: &str,
|
||||
event: &WorkflowRunEvent,
|
||||
) -> Result<()> {
|
||||
let envelope = build_event_envelope(event, run_id);
|
||||
let line = serde_json::to_string(&envelope)?;
|
||||
let line = fabro_util::redact::redact_jsonl_line(&line);
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(run_dir.join("progress.jsonl"))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to open {}",
|
||||
run_dir.join("progress.jsonl").display()
|
||||
)
|
||||
})?;
|
||||
writeln!(file, "{line}")?;
|
||||
|
||||
let pretty = serde_json::to_string_pretty(&envelope)?;
|
||||
let pretty = fabro_util::redact::redact_jsonl_line(&pretty);
|
||||
std::fs::write(run_dir.join("live.json"), pretty)
|
||||
.with_context(|| format!("Failed to write {}", run_dir.join("live.json").display()))?;
|
||||
|
||||
Ok(())
|
||||
fabro_workflows::event::append_progress_event(run_dir, run_id, event)
|
||||
}
|
||||
|
||||
pub(crate) fn append_run_notice(
|
||||
|
|
|
|||
|
|
@ -7,23 +7,18 @@ use std::time::Instant;
|
|||
use anyhow::{bail, Context};
|
||||
use chrono::Local;
|
||||
use clap::{Args, ValueEnum};
|
||||
use fabro_agent::{
|
||||
DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox, WorktreeConfig, WorktreeSandbox,
|
||||
};
|
||||
use fabro_agent::{DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox};
|
||||
use fabro_config::config::FabroConfig;
|
||||
use fabro_config::{project as project_config, run as run_config, sandbox as sandbox_config};
|
||||
use fabro_interview::{AutoApproveInterviewer, ConsoleInterviewer, FileInterviewer, Interviewer};
|
||||
use fabro_model::{Catalog, FallbackTarget, Provider};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflows::devcontainer_bridge;
|
||||
use fabro_workflows::event::{EventEmitter, RunNoticeLevel, WorkflowRunEvent};
|
||||
use fabro_workflows::event::EventEmitter;
|
||||
use fabro_workflows::git::GitSyncStatus;
|
||||
use fabro_workflows::handler::default_registry;
|
||||
use fabro_workflows::handler::llm::{AgentApiBackend, AgentCliBackend, BackendRouter};
|
||||
use fabro_workflows::operations::{
|
||||
resume as operations_resume, start, StartFinalizeOptions, StartOptions, StartPullRequestConfig,
|
||||
StartRetroOptions,
|
||||
resume as operations_resume, start, DevcontainerSpec, LlmSpec, SandboxEnvSpec, SandboxSpec,
|
||||
StartFinalizeOptions, StartOptions, StartPullRequestConfig, StartRetroOptions,
|
||||
};
|
||||
use fabro_workflows::outcome::StageStatus;
|
||||
use fabro_workflows::outcome::{compute_stage_cost, format_cost};
|
||||
|
|
@ -31,12 +26,12 @@ use fabro_workflows::pipeline::{
|
|||
build_conclusion, classify_engine_result, persist_terminal_outcome, Persisted, Validated,
|
||||
};
|
||||
use fabro_workflows::records::Checkpoint;
|
||||
use fabro_workflows::run_options::{GitCheckpointOptions, LifecycleOptions};
|
||||
use fabro_workflows::run_options::LifecycleOptions;
|
||||
use indicatif::HumanDuration;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
|
||||
use super::detached_support::{self, DetachedRunBootstrapGuard, DetachedRunCompletionGuard};
|
||||
use super::detached_support::{DetachedRunBootstrapGuard, DetachedRunCompletionGuard};
|
||||
use super::run_progress;
|
||||
use crate::commands::shared::{
|
||||
format_tokens_human, print_diagnostics, read_workflow_file, relative_path, tilde_path,
|
||||
|
|
@ -427,17 +422,6 @@ pub(crate) async fn mint_github_token(
|
|||
Ok(token)
|
||||
}
|
||||
|
||||
/// How the workflow run's working directory is set up.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum WorkdirStrategy {
|
||||
/// Run directly in the current working directory.
|
||||
LocalDirectory,
|
||||
/// Create a local git worktree for isolation.
|
||||
LocalWorktree,
|
||||
/// Remote sandbox clones from origin (Daytona, Exe, SSH).
|
||||
Cloud,
|
||||
}
|
||||
|
||||
/// Accumulates token usage and cost across all workflow stages.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CostAccumulator {
|
||||
|
|
@ -450,18 +434,6 @@ pub(crate) struct CostAccumulator {
|
|||
pub has_pricing: bool,
|
||||
}
|
||||
|
||||
/// Create a [`LocalSandbox`] wired to emit [`WorkflowRunEvent::Sandbox`] events.
|
||||
pub(crate) fn local_sandbox_with_callback(
|
||||
cwd: PathBuf,
|
||||
emitter: Arc<EventEmitter>,
|
||||
) -> Arc<dyn Sandbox> {
|
||||
let mut env = LocalSandbox::new(cwd);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter.emit(&fabro_workflows::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
}
|
||||
|
||||
pub(crate) const RUN_GRAPH_FILE: &str = "workflow.fabro";
|
||||
pub(crate) const RUN_CONFIG_FILE: &str = "workflow.toml";
|
||||
|
||||
|
|
@ -1132,39 +1104,6 @@ async fn run_command_impl(
|
|||
}
|
||||
});
|
||||
|
||||
// JSONL progress log + live.json snapshot
|
||||
{
|
||||
let jsonl_path = run_dir.join("progress.jsonl");
|
||||
let live_path = run_dir.join("live.json");
|
||||
let run_id = Arc::new(Mutex::new(run_id.clone()));
|
||||
let run_id_clone = Arc::clone(&run_id);
|
||||
emitter.on_event(move |event| {
|
||||
if let fabro_workflows::event::WorkflowRunEvent::WorkflowRunStarted { run_id, .. } =
|
||||
event
|
||||
{
|
||||
*run_id_clone.lock().unwrap() = run_id.clone();
|
||||
}
|
||||
let envelope = build_event_envelope(event, &run_id_clone.lock().unwrap());
|
||||
// Append to progress.jsonl
|
||||
if let Ok(line) = serde_json::to_string(&envelope) {
|
||||
let line = fabro_util::redact::redact_jsonl_line(&line);
|
||||
use std::io::Write;
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&jsonl_path)
|
||||
{
|
||||
let _ = writeln!(f, "{line}");
|
||||
}
|
||||
}
|
||||
// Overwrite live.json
|
||||
if let Ok(pretty) = serde_json::to_string_pretty(&envelope) {
|
||||
let pretty = fabro_util::redact::redact_jsonl_line(&pretty);
|
||||
let _ = std::fs::write(&live_path, pretty);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run_progress::ProgressUI::register(&progress_ui, &emitter);
|
||||
|
||||
// 4. Build interviewer
|
||||
|
|
@ -1181,479 +1120,13 @@ async fn run_command_impl(
|
|||
))
|
||||
};
|
||||
|
||||
// Determine the working directory strategy.
|
||||
// Only the Local provider supports git worktrees on the host.
|
||||
// Remote sandboxes (Daytona, Exe, SSH) clone from origin inside the sandbox.
|
||||
// Docker uses the bind-mounted host directory as-is.
|
||||
// Resume runs skip worktree creation — the engine runs in the original
|
||||
// working directory and the checkpoint restores logical state.
|
||||
let workdir_strategy = if resume {
|
||||
match sandbox_provider {
|
||||
SandboxProvider::Local | SandboxProvider::Docker => WorkdirStrategy::LocalDirectory,
|
||||
_ => WorkdirStrategy::Cloud,
|
||||
}
|
||||
} else {
|
||||
match sandbox_provider {
|
||||
SandboxProvider::Local => {
|
||||
let worktree_mode = resolve_worktree_mode(run_cfg.as_ref(), &run_defaults);
|
||||
match worktree_mode {
|
||||
sandbox_config::WorktreeMode::Always => WorkdirStrategy::LocalWorktree,
|
||||
sandbox_config::WorktreeMode::Clean => {
|
||||
if git_status.is_clean() {
|
||||
WorkdirStrategy::LocalWorktree
|
||||
} else {
|
||||
WorkdirStrategy::LocalDirectory
|
||||
}
|
||||
}
|
||||
sandbox_config::WorktreeMode::Dirty => {
|
||||
if git_status.is_clean() {
|
||||
WorkdirStrategy::LocalDirectory
|
||||
} else {
|
||||
WorkdirStrategy::LocalWorktree
|
||||
}
|
||||
}
|
||||
sandbox_config::WorktreeMode::Never => WorkdirStrategy::LocalDirectory,
|
||||
}
|
||||
}
|
||||
SandboxProvider::Docker => WorkdirStrategy::LocalDirectory,
|
||||
_ => WorkdirStrategy::Cloud,
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
?workdir_strategy,
|
||||
?sandbox_provider,
|
||||
?git_status,
|
||||
"Resolved workdir strategy"
|
||||
);
|
||||
|
||||
// Warn about uncommitted changes that won't be available in the execution environment.
|
||||
if git_status == GitSyncStatus::Dirty {
|
||||
let env_name = match workdir_strategy {
|
||||
WorkdirStrategy::LocalWorktree => Some("worktree"),
|
||||
WorkdirStrategy::Cloud => Some("remote sandbox"),
|
||||
WorkdirStrategy::LocalDirectory => None,
|
||||
};
|
||||
if let Some(env_name) = env_name {
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"dirty_worktree",
|
||||
format!("Uncommitted changes will not be included in the {env_name}."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-push when the execution environment needs commits on the remote.
|
||||
if !dry_run_flag
|
||||
&& matches!(
|
||||
workdir_strategy,
|
||||
WorkdirStrategy::LocalWorktree | WorkdirStrategy::Cloud
|
||||
)
|
||||
{
|
||||
if let Some(ref branch) = detected_base_branch {
|
||||
// For Synced we know no push is needed; for Unsynced we know it is;
|
||||
// for Dirty the push status wasn't checked, so check now.
|
||||
let needs_push = match git_status {
|
||||
GitSyncStatus::Synced => false,
|
||||
GitSyncStatus::Unsynced => true,
|
||||
GitSyncStatus::Dirty => {
|
||||
let check_repo = original_cwd.clone();
|
||||
let check_branch = branch.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
fabro_workflows::git::branch_needs_push(
|
||||
&check_repo,
|
||||
"origin",
|
||||
&check_branch,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap_or(true)
|
||||
}
|
||||
};
|
||||
|
||||
if needs_push {
|
||||
let repo_path = original_cwd.clone();
|
||||
let branch_owned = branch.clone();
|
||||
let result = fabro_workflows::git::blocking_push_with_timeout(60, move || {
|
||||
fabro_workflows::git::push_branch(&repo_path, "origin", &branch_owned)
|
||||
})
|
||||
.await;
|
||||
match result {
|
||||
Ok(()) => {
|
||||
tracing::info!(%branch, "Pushed current branch to origin");
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Info,
|
||||
"git_push_succeeded",
|
||||
format!("{branch} (synced local commits to remote)"),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, %branch, "Failed to push current branch");
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"git_push_failed",
|
||||
format!("Failed to push {branch} to origin: {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::info!(%branch, "Branch already in sync with origin, skipping push");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute worktree configuration for local isolation.
|
||||
// The actual git setup (branch, worktree add, reset) happens inside the sandbox
|
||||
// creation block for SandboxProvider::Local below.
|
||||
let (mut worktree_path, mut worktree_branch, mut worktree_base_sha) = if workdir_strategy
|
||||
== WorkdirStrategy::LocalWorktree
|
||||
{
|
||||
match fabro_workflows::git::head_sha(&original_cwd) {
|
||||
Ok(base_sha) => {
|
||||
let branch_name = format!("{}{run_id}", fabro_workflows::git::RUN_BRANCH_PREFIX);
|
||||
let wt_path = run_dir.join("worktree");
|
||||
(Some(wt_path), Some(branch_name), Some(base_sha))
|
||||
}
|
||||
Err(e) => {
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"worktree_setup_failed",
|
||||
format!("Git worktree setup failed ({e}), running without worktree."),
|
||||
);
|
||||
(None, None, None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
if let Some(ref wt) = worktree_path {
|
||||
progress_ui
|
||||
.lock()
|
||||
.expect("progress lock poisoned")
|
||||
.show_worktree(wt);
|
||||
}
|
||||
|
||||
// Show base SHA for both worktree and cloud strategies.
|
||||
let base_sha_display = worktree_base_sha.clone().or_else(|| {
|
||||
if workdir_strategy == WorkdirStrategy::Cloud {
|
||||
fabro_workflows::git::head_sha(&original_cwd).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(ref sha) = base_sha_display {
|
||||
progress_ui
|
||||
.lock()
|
||||
.expect("progress lock poisoned")
|
||||
.show_base_info(detected_base_branch.as_deref(), sha);
|
||||
}
|
||||
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let mut daytona_config = resolve_daytona_config(run_cfg.as_ref(), &run_defaults);
|
||||
let daytona_config = resolve_daytona_config(run_cfg.as_ref(), &run_defaults);
|
||||
#[cfg(feature = "exedev")]
|
||||
let exe_config = resolve_exe_config(run_cfg.as_ref(), &run_defaults);
|
||||
let ssh_config = resolve_ssh_config(run_cfg.as_ref(), &run_defaults);
|
||||
|
||||
// Resolve devcontainer if enabled
|
||||
let devcontainer_config = if run_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.or(run_defaults.sandbox.as_ref())
|
||||
.and_then(|s| s.devcontainer)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
match fabro_devcontainer::DevcontainerResolver::resolve(&cwd).await {
|
||||
Ok(dc) => {
|
||||
let lifecycle_command_count = dc.on_create_commands.len()
|
||||
+ dc.post_create_commands.len()
|
||||
+ dc.post_start_commands.len();
|
||||
emitter.emit(
|
||||
&fabro_workflows::event::WorkflowRunEvent::DevcontainerResolved {
|
||||
dockerfile_lines: dc.dockerfile.lines().count(),
|
||||
environment_count: dc.environment.len(),
|
||||
lifecycle_command_count,
|
||||
workspace_folder: dc.workspace_folder.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Override daytona_config with devcontainer dockerfile
|
||||
let snapshot = devcontainer_bridge::devcontainer_to_snapshot_config(&dc);
|
||||
let mut cfg = daytona_config.unwrap_or_default();
|
||||
cfg.snapshot = Some(snapshot);
|
||||
daytona_config = Some(cfg);
|
||||
|
||||
// Run initialize_commands on host
|
||||
let timeout = std::time::Duration::from_millis(300_000);
|
||||
for cmd in &dc.initialize_commands {
|
||||
let shell_cmds = match cmd {
|
||||
fabro_devcontainer::Command::Shell(s) => vec![s.clone()],
|
||||
fabro_devcontainer::Command::Args(args) => {
|
||||
vec![args
|
||||
.iter()
|
||||
.map(|a| {
|
||||
shlex::try_quote(a).unwrap_or_else(|_| a.into()).to_string()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")]
|
||||
}
|
||||
fabro_devcontainer::Command::Parallel(map) => {
|
||||
map.values().cloned().collect()
|
||||
}
|
||||
};
|
||||
for shell_cmd in &shell_cmds {
|
||||
let fut = tokio::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(shell_cmd)
|
||||
.current_dir(&cwd)
|
||||
.output();
|
||||
let output = tokio::time::timeout(timeout, fut)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Devcontainer initializeCommand timed out: {shell_cmd}")
|
||||
})?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to execute devcontainer initializeCommand: {shell_cmd}"
|
||||
)
|
||||
})?;
|
||||
if !output.status.success() {
|
||||
let code = output
|
||||
.status
|
||||
.code()
|
||||
.map_or("unknown".to_string(), |c| c.to_string());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!(
|
||||
"Devcontainer initializeCommand failed (exit code {code}): {shell_cmd}\n{stderr}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(dc)
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("Failed to resolve devcontainer: {e}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Deferred sandbox reference — filled after sandbox creation, consumed by event listeners.
|
||||
let deferred_sandbox: Arc<Mutex<Option<Arc<dyn Sandbox>>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
// Register SandboxInitialized listener (must happen before emitter is wrapped in Arc)
|
||||
{
|
||||
let run_dir_for_listener = run_dir.clone();
|
||||
let progress_for_listener = Arc::clone(&progress_ui);
|
||||
let cwd_for_listener = cwd.to_string_lossy().to_string();
|
||||
let ssh_data_host = ssh_config.as_ref().map(|c| c.destination.clone());
|
||||
let deferred_sb = Arc::clone(&deferred_sandbox);
|
||||
let provider = sandbox_provider; // Copy — captured by move closure
|
||||
emitter.on_event(move |event| {
|
||||
if let fabro_workflows::event::WorkflowRunEvent::SandboxInitialized {
|
||||
working_directory,
|
||||
} = event
|
||||
{
|
||||
progress_for_listener
|
||||
.lock()
|
||||
.expect("progress lock poisoned")
|
||||
.set_working_directory(working_directory.clone());
|
||||
|
||||
// Build sandbox record from template
|
||||
let sandbox_info_opt = deferred_sb.lock().unwrap().as_ref().and_then(|sb| {
|
||||
let info = sb.sandbox_info();
|
||||
if info.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(info)
|
||||
}
|
||||
});
|
||||
|
||||
let is_docker = provider == SandboxProvider::Docker;
|
||||
let record = fabro_sandbox::SandboxRecord {
|
||||
provider: provider.to_string(),
|
||||
working_directory: working_directory.clone(),
|
||||
identifier: sandbox_info_opt,
|
||||
host_working_directory: if is_docker {
|
||||
Some(cwd_for_listener.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
container_mount_point: if is_docker {
|
||||
Some(working_directory.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
data_host: if provider == SandboxProvider::Ssh {
|
||||
ssh_data_host.clone()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
if let Err(e) = record.save(&run_dir_for_listener.join("sandbox.json")) {
|
||||
tracing::warn!(error = %e, "Failed to save sandbox record");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wrap emitter in Arc so we can share it with exec env callbacks
|
||||
let emitter = Arc::new(emitter);
|
||||
|
||||
let sandbox: Arc<dyn Sandbox> = match sandbox_provider {
|
||||
SandboxProvider::Docker => {
|
||||
let config = DockerSandboxConfig {
|
||||
host_working_directory: cwd.to_string_lossy().to_string(),
|
||||
..DockerSandboxConfig::default()
|
||||
};
|
||||
let mut env = DockerSandbox::new(config)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create Docker environment: {e}"))?;
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&fabro_workflows::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
}
|
||||
SandboxProvider::Daytona => {
|
||||
let config = daytona_config.clone().unwrap_or_default();
|
||||
let mut env = fabro_sandbox::daytona::DaytonaSandbox::new(
|
||||
config,
|
||||
github_app.clone(),
|
||||
Some(run_id.clone()),
|
||||
detected_base_branch.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&fabro_workflows::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
}
|
||||
#[cfg(feature = "exedev")]
|
||||
SandboxProvider::Exe => {
|
||||
let clone_params = resolve_exe_clone_params(&original_cwd);
|
||||
|
||||
let mgmt_ssh = fabro_sandbox::exe::OpensshRunner::connect_raw("exe.dev")
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to exe.dev: {e}"))?;
|
||||
let config = exe_config.unwrap_or_default();
|
||||
let mut env = fabro_sandbox::exe::ExeSandbox::new(
|
||||
Box::new(mgmt_ssh),
|
||||
config,
|
||||
clone_params,
|
||||
Some(run_id.clone()),
|
||||
github_app.clone(),
|
||||
);
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&fabro_workflows::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
}
|
||||
#[cfg(not(feature = "exedev"))]
|
||||
SandboxProvider::Exe => {
|
||||
bail!("exe sandbox requires the exedev feature");
|
||||
}
|
||||
SandboxProvider::Ssh => {
|
||||
let config = ssh_config
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("--sandbox ssh requires [sandbox.ssh] config"))?;
|
||||
let clone_params = resolve_ssh_clone_params(&original_cwd);
|
||||
let mut env = fabro_sandbox::ssh::SshSandbox::new(
|
||||
config,
|
||||
clone_params,
|
||||
Some(run_id.clone()),
|
||||
github_app.clone(),
|
||||
);
|
||||
let emitter_cb = Arc::clone(&emitter);
|
||||
env.set_event_callback(Arc::new(move |event| {
|
||||
emitter_cb.emit(&fabro_workflows::event::WorkflowRunEvent::Sandbox { event });
|
||||
}));
|
||||
Arc::new(env)
|
||||
}
|
||||
SandboxProvider::Local => {
|
||||
if let (Some(base_sha), Some(branch_name), Some(wt_path)) = (
|
||||
worktree_base_sha.as_ref(),
|
||||
worktree_branch.as_ref(),
|
||||
worktree_path.as_ref(),
|
||||
) {
|
||||
// Set up a WorktreeSandbox for git-isolated local execution.
|
||||
let wt_path_str = wt_path.to_string_lossy().into_owned();
|
||||
let inner = local_sandbox_with_callback(original_cwd.clone(), Arc::clone(&emitter));
|
||||
let wt_config = WorktreeConfig {
|
||||
branch_name: branch_name.clone(),
|
||||
base_sha: base_sha.clone(),
|
||||
worktree_path: wt_path_str,
|
||||
skip_branch_creation: false,
|
||||
};
|
||||
let mut wt_sandbox = WorktreeSandbox::new(inner, wt_config);
|
||||
wt_sandbox.set_event_callback(Arc::clone(&emitter).worktree_callback());
|
||||
|
||||
match wt_sandbox.initialize().await {
|
||||
Ok(()) => {
|
||||
std::env::set_current_dir(wt_path)?;
|
||||
Arc::new(wt_sandbox) as Arc<dyn Sandbox>
|
||||
}
|
||||
Err(e) => {
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"worktree_setup_failed",
|
||||
format!("Git worktree setup failed ({e}), running without worktree."),
|
||||
);
|
||||
// Reset so RunOptions does not enable git checkpointing
|
||||
worktree_path = None;
|
||||
worktree_branch = None;
|
||||
worktree_base_sha = None;
|
||||
local_sandbox_with_callback(cwd.clone(), Arc::clone(&emitter))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
local_sandbox_with_callback(cwd.clone(), Arc::clone(&emitter))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Wrap with ReadBeforeWriteSandbox to enforce read-before-write guard
|
||||
// (delegate_sandbox! macro delegates initialize/cleanup)
|
||||
let sandbox: Arc<dyn Sandbox> = Arc::new(fabro_agent::ReadBeforeWriteSandbox::new(sandbox));
|
||||
|
||||
// Fill deferred sandbox reference for event listeners registered above
|
||||
*deferred_sandbox.lock().unwrap() = Some(Arc::clone(&sandbox));
|
||||
|
||||
// 6. Resolve backend, model, and provider
|
||||
let (dry_run_mode, llm_client) = if dry_run_flag {
|
||||
(true, None)
|
||||
} else {
|
||||
match fabro_llm::client::Client::from_env().await {
|
||||
Ok(c) if c.provider_names().is_empty() => {
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"dry_run_no_llm",
|
||||
"No LLM providers configured. Running in dry-run mode.",
|
||||
);
|
||||
(true, None)
|
||||
}
|
||||
Ok(c) => (false, Some(c)),
|
||||
Err(e) => {
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"dry_run_llm_init_failed",
|
||||
format!("Failed to initialize LLM client: {e}. Running in dry-run mode."),
|
||||
);
|
||||
(true, None)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Parse provider string to enum (defaults to best available from env)
|
||||
let provider_enum: Provider = provider
|
||||
.as_deref()
|
||||
|
|
@ -1662,62 +1135,7 @@ async fn run_command_impl(
|
|||
.map_err(|e| anyhow::anyhow!("{e}"))?
|
||||
.unwrap_or_else(Provider::default_from_env);
|
||||
|
||||
// Resolve fallback chain from config
|
||||
let fallback_chain = resolve_fallback_chain(provider_enum, &model, run_cfg.as_ref());
|
||||
|
||||
// 7. Build engine
|
||||
// Devcontainer env is layered underneath TOML env (TOML wins on conflict)
|
||||
let sandbox_env: HashMap<String, String> = {
|
||||
let mut env = if let Some(ref dc) = devcontainer_config {
|
||||
dc.environment.clone()
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
if let Some(mut toml_env) = run_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.or(run_defaults.sandbox.as_ref())
|
||||
.and_then(|s| s.env.clone())
|
||||
{
|
||||
// When falling back to run_defaults (run_cfg is None, i.e. bare .fabro
|
||||
// workflow), env refs haven't been resolved yet — resolve them now.
|
||||
if run_cfg.is_none() {
|
||||
run_config::resolve_env_refs(&mut toml_env)?;
|
||||
}
|
||||
env.extend(toml_env);
|
||||
}
|
||||
env
|
||||
};
|
||||
|
||||
// Mint a GitHub App IAT and inject as GITHUB_TOKEN if [github] permissions are declared
|
||||
let mut sandbox_env = sandbox_env;
|
||||
let github_permissions = run_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.github.as_ref())
|
||||
.or(run_defaults.github.as_ref());
|
||||
if let Some(gh_cfg) = github_permissions {
|
||||
if !gh_cfg.permissions.is_empty() {
|
||||
if let (Some(ref creds), Some(ref url)) = (&github_app, &origin_url) {
|
||||
match mint_github_token(creds, url, &gh_cfg.permissions).await {
|
||||
Ok(token) => {
|
||||
debug!("Minted GitHub IAT for sandbox GITHUB_TOKEN");
|
||||
sandbox_env.insert("GITHUB_TOKEN".to_string(), token);
|
||||
}
|
||||
Err(e) => {
|
||||
emit_run_notice(
|
||||
&emitter,
|
||||
RunNoticeLevel::Warn,
|
||||
"github_token_failed",
|
||||
format!("Failed to mint GitHub token: {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Skipping GitHub token: no GitHub App credentials or origin URL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mcp_servers: Vec<fabro_mcp::config::McpServerConfig> = {
|
||||
let servers = run_cfg
|
||||
.as_ref()
|
||||
|
|
@ -1733,66 +1151,102 @@ async fn run_command_impl(
|
|||
)
|
||||
.collect()
|
||||
};
|
||||
let registry = default_registry(interviewer.clone(), {
|
||||
let sandbox_env = sandbox_env.clone();
|
||||
let model = model.clone();
|
||||
let mcp_servers = mcp_servers.clone();
|
||||
move || {
|
||||
if dry_run_mode {
|
||||
None
|
||||
} else {
|
||||
let api =
|
||||
AgentApiBackend::new(model.clone(), provider_enum, fallback_chain.clone())
|
||||
.with_env(sandbox_env.clone())
|
||||
.with_mcp_servers(mcp_servers.clone());
|
||||
let cli = AgentCliBackend::new(model.clone(), provider_enum)
|
||||
.with_env(sandbox_env.clone());
|
||||
Some(Box::new(BackendRouter::new(Box::new(api), cli)))
|
||||
}
|
||||
let sandbox_spec = match sandbox_provider {
|
||||
SandboxProvider::Local => SandboxSpec::Local {
|
||||
working_directory: cwd.clone(),
|
||||
},
|
||||
SandboxProvider::Docker => SandboxSpec::Docker {
|
||||
config: DockerSandboxConfig {
|
||||
host_working_directory: cwd.to_string_lossy().to_string(),
|
||||
..DockerSandboxConfig::default()
|
||||
},
|
||||
},
|
||||
SandboxProvider::Daytona => SandboxSpec::Daytona {
|
||||
config: daytona_config.unwrap_or_default(),
|
||||
github_app: github_app.clone(),
|
||||
run_id: Some(run_id.clone()),
|
||||
clone_branch: detected_base_branch.clone(),
|
||||
},
|
||||
#[cfg(feature = "exedev")]
|
||||
SandboxProvider::Exe => SandboxSpec::Exe {
|
||||
config: exe_config.unwrap_or_default(),
|
||||
clone_params: resolve_exe_clone_params(&original_cwd),
|
||||
run_id: Some(run_id.clone()),
|
||||
github_app: github_app.clone(),
|
||||
mgmt_destination: "exe.dev".to_string(),
|
||||
},
|
||||
#[cfg(not(feature = "exedev"))]
|
||||
SandboxProvider::Exe => {
|
||||
bail!("exe sandbox requires the exedev feature");
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Execute
|
||||
// Set up metadata branch for git checkpointing (host or remote — engine fills remote)
|
||||
let git = if worktree_path.is_some() {
|
||||
Some(GitCheckpointOptions {
|
||||
base_sha: worktree_base_sha,
|
||||
run_branch: worktree_branch,
|
||||
meta_branch: Some(fabro_workflows::git::MetadataStore::branch_name(&run_id)),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
SandboxProvider::Ssh => SandboxSpec::Ssh {
|
||||
config: ssh_config
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("--sandbox ssh requires [sandbox.ssh] config"))?,
|
||||
clone_params: resolve_ssh_clone_params(&original_cwd),
|
||||
run_id: Some(run_id.clone()),
|
||||
github_app: github_app.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
// Build lifecycle config for sandbox init, setup commands, and devcontainer phases
|
||||
let toml_env = if let Some(mut env) = run_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.or(run_defaults.sandbox.as_ref())
|
||||
.and_then(|s| s.env.clone())
|
||||
{
|
||||
if run_cfg.is_none() {
|
||||
run_config::resolve_env_refs(&mut env)?;
|
||||
}
|
||||
env
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
let sandbox_env = SandboxEnvSpec {
|
||||
devcontainer_env: HashMap::new(),
|
||||
toml_env,
|
||||
github_permissions: run_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.github.as_ref())
|
||||
.or(run_defaults.github.as_ref())
|
||||
.and_then(|cfg| (!cfg.permissions.is_empty()).then(|| cfg.permissions.clone())),
|
||||
origin_url: origin_url.clone(),
|
||||
};
|
||||
|
||||
let devcontainer_enabled = run_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.sandbox.as_ref())
|
||||
.or(run_defaults.sandbox.as_ref())
|
||||
.and_then(|s| s.devcontainer)
|
||||
.unwrap_or(false);
|
||||
|
||||
let llm = LlmSpec {
|
||||
model: model.clone(),
|
||||
provider: provider_enum,
|
||||
fallback_chain,
|
||||
mcp_servers,
|
||||
dry_run: dry_run_flag,
|
||||
};
|
||||
|
||||
let worktree_mode = resolve_worktree_mode(run_cfg.as_ref(), &run_defaults);
|
||||
let lifecycle = LifecycleOptions {
|
||||
setup_commands,
|
||||
setup_command_timeout_ms: 300_000,
|
||||
devcontainer_phases: if let Some(ref dc) = devcontainer_config {
|
||||
vec![
|
||||
("on_create".to_string(), dc.on_create_commands.clone()),
|
||||
("post_create".to_string(), dc.post_create_commands.clone()),
|
||||
("post_start".to_string(), dc.post_start_commands.clone()),
|
||||
]
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
devcontainer_phases: Vec::new(),
|
||||
};
|
||||
|
||||
// Defuse the bootstrap guard — engine.run() has taken ownership of lifecycle status.
|
||||
status_guard.defuse();
|
||||
|
||||
let run_start = Instant::now();
|
||||
let pr_config = if dry_run_mode {
|
||||
None
|
||||
} else {
|
||||
persisted.run_record().config.pull_request.clone()
|
||||
};
|
||||
let pr_config = persisted.run_record().config.pull_request.clone();
|
||||
let start_options = StartOptions {
|
||||
cancel_token: None,
|
||||
emitter: Arc::clone(&emitter),
|
||||
sandbox: Arc::clone(&sandbox),
|
||||
registry: Arc::new(registry),
|
||||
sandbox: sandbox_spec,
|
||||
llm,
|
||||
interviewer: interviewer.clone(),
|
||||
lifecycle,
|
||||
hooks: fabro_hooks::HookConfig {
|
||||
hooks: run_cfg
|
||||
|
|
@ -1801,17 +1255,18 @@ async fn run_command_impl(
|
|||
.unwrap_or_else(|| run_defaults.hooks.clone()),
|
||||
},
|
||||
sandbox_env,
|
||||
devcontainer: devcontainer_enabled.then(|| DevcontainerSpec {
|
||||
enabled: true,
|
||||
resolve_dir: cwd.clone(),
|
||||
}),
|
||||
seed_context: None,
|
||||
git_author,
|
||||
git,
|
||||
git: None,
|
||||
github_app: github_app.clone(),
|
||||
dry_run: dry_run_mode,
|
||||
worktree_mode: Some(worktree_mode),
|
||||
dry_run: dry_run_flag,
|
||||
retro: StartRetroOptions {
|
||||
enabled: !no_retro_flag && project_config::is_retro_enabled(),
|
||||
dry_run: dry_run_mode,
|
||||
llm_client: llm_client.clone(),
|
||||
provider: provider_enum,
|
||||
model: model.clone(),
|
||||
},
|
||||
finalize: StartFinalizeOptions { preserve_sandbox },
|
||||
pull_request: StartPullRequestConfig {
|
||||
|
|
@ -1882,19 +1337,6 @@ async fn run_command_impl(
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit_run_notice(
|
||||
emitter: &EventEmitter,
|
||||
level: RunNoticeLevel,
|
||||
code: impl Into<String>,
|
||||
message: impl Into<String>,
|
||||
) {
|
||||
emitter.emit(&WorkflowRunEvent::RunNotice {
|
||||
level,
|
||||
code: code.into(),
|
||||
message: message.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Print a summary of the completed run from `conclusion.json` and `pull_request.json`.
|
||||
///
|
||||
/// Used by the unified create+start+attach path in `main.rs` to display
|
||||
|
|
@ -2468,11 +1910,12 @@ async fn run_preflight(
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn build_event_envelope(
|
||||
event: &fabro_workflows::event::WorkflowRunEvent,
|
||||
run_id: &str,
|
||||
) -> serde_json::Value {
|
||||
detached_support::build_event_envelope(event, run_id)
|
||||
fabro_workflows::event::build_event_envelope(event, run_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -311,6 +311,19 @@ impl ProgressUI {
|
|||
|
||||
pub(crate) fn handle_event(&mut self, event: &WorkflowRunEvent) {
|
||||
match event {
|
||||
WorkflowRunEvent::WorkflowRunStarted {
|
||||
base_branch,
|
||||
base_sha,
|
||||
worktree_dir,
|
||||
..
|
||||
} => {
|
||||
if let Some(worktree_dir) = worktree_dir {
|
||||
self.show_worktree(std::path::Path::new(worktree_dir));
|
||||
}
|
||||
if let Some(base_sha) = base_sha {
|
||||
self.show_base_info(base_branch.as_deref(), base_sha);
|
||||
}
|
||||
}
|
||||
WorkflowRunEvent::Sandbox {
|
||||
event: sandbox_event,
|
||||
} => {
|
||||
|
|
@ -662,6 +675,14 @@ impl ProgressUI {
|
|||
|key: &str| -> u64 { envelope.get(key).and_then(|v| v.as_u64()).unwrap_or(0) };
|
||||
|
||||
match event_name {
|
||||
"WorkflowRunStarted" => {
|
||||
if let Some(worktree_dir) = str_field("worktree_dir") {
|
||||
self.show_worktree(std::path::Path::new(worktree_dir));
|
||||
}
|
||||
if let Some(base_sha) = str_field("base_sha") {
|
||||
self.show_base_info(str_field("base_branch"), base_sha);
|
||||
}
|
||||
}
|
||||
"Sandbox.Initializing" => {
|
||||
let provider = str_field("sandbox_provider")
|
||||
.unwrap_or("unknown")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::outcome::StageUsage;
|
||||
|
|
@ -21,6 +25,8 @@ pub enum WorkflowRunEvent {
|
|||
name: String,
|
||||
run_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
base_branch: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
base_sha: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
run_branch: Option<String>,
|
||||
|
|
@ -791,6 +797,80 @@ pub fn flatten_event(
|
|||
(event_name, fields)
|
||||
}
|
||||
|
||||
pub fn build_event_envelope(event: &WorkflowRunEvent, run_id: &str) -> serde_json::Value {
|
||||
let (event_name, event_fields) = flatten_event(event);
|
||||
let mut envelope = serde_json::Map::new();
|
||||
envelope.insert(
|
||||
"ts".to_string(),
|
||||
serde_json::Value::String(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)),
|
||||
);
|
||||
envelope.insert(
|
||||
"run_id".to_string(),
|
||||
serde_json::Value::String(run_id.to_string()),
|
||||
);
|
||||
envelope.insert("event".to_string(), serde_json::Value::String(event_name));
|
||||
for (k, v) in event_fields {
|
||||
if k != "ts" && k != "run_id" && k != "event" {
|
||||
envelope.insert(k, v);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(envelope)
|
||||
}
|
||||
|
||||
pub fn append_progress_event(run_dir: &Path, run_id: &str, event: &WorkflowRunEvent) -> Result<()> {
|
||||
let envelope = build_event_envelope(event, run_id);
|
||||
let line = serde_json::to_string(&envelope)?;
|
||||
let line = fabro_util::redact::redact_jsonl_line(&line);
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(run_dir.join("progress.jsonl"))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to open {}",
|
||||
run_dir.join("progress.jsonl").display()
|
||||
)
|
||||
})?;
|
||||
writeln!(file, "{line}")?;
|
||||
|
||||
let pretty = serde_json::to_string_pretty(&envelope)?;
|
||||
let pretty = fabro_util::redact::redact_jsonl_line(&pretty);
|
||||
std::fs::write(run_dir.join("live.json"), pretty)
|
||||
.with_context(|| format!("Failed to write {}", run_dir.join("live.json").display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct ProgressLogger {
|
||||
run_dir: PathBuf,
|
||||
run_id: String,
|
||||
}
|
||||
|
||||
impl ProgressLogger {
|
||||
#[must_use]
|
||||
pub fn new(run_dir: impl Into<PathBuf>, run_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
run_dir: run_dir.into(),
|
||||
run_id: run_id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(self, emitter: &EventEmitter) {
|
||||
let run_dir = self.run_dir;
|
||||
let run_id = Arc::new(std::sync::Mutex::new(self.run_id));
|
||||
emitter.on_event(move |event| {
|
||||
if let WorkflowRunEvent::WorkflowRunStarted {
|
||||
run_id: started_run_id,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
*run_id.lock().unwrap() = started_run_id.clone();
|
||||
}
|
||||
let _ = append_progress_event(&run_dir, &run_id.lock().unwrap(), event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_agent(inner: serde_json::Value) -> (String, serde_json::Map<String, serde_json::Value>) {
|
||||
let serde_json::Value::Object(mut agent_fields) = inner else {
|
||||
return ("Agent".to_string(), serde_json::Map::new());
|
||||
|
|
@ -1136,6 +1216,7 @@ mod tests {
|
|||
emitter.emit(&WorkflowRunEvent::WorkflowRunStarted {
|
||||
name: "test".to_string(),
|
||||
run_id: "1".to_string(),
|
||||
base_branch: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
worktree_dir: None,
|
||||
|
|
@ -1636,6 +1717,7 @@ mod tests {
|
|||
emitter.emit(&WorkflowRunEvent::WorkflowRunStarted {
|
||||
name: "test".to_string(),
|
||||
run_id: "1".to_string(),
|
||||
base_branch: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
worktree_dir: None,
|
||||
|
|
@ -1823,6 +1905,7 @@ mod tests {
|
|||
let event = WorkflowRunEvent::WorkflowRunStarted {
|
||||
name: "my_pipeline".to_string(),
|
||||
run_id: "r1".to_string(),
|
||||
base_branch: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
worktree_dir: None,
|
||||
|
|
@ -2447,6 +2530,7 @@ mod tests {
|
|||
let event = WorkflowRunEvent::WorkflowRunStarted {
|
||||
name: "my_workflow".to_string(),
|
||||
run_id: "r42".to_string(),
|
||||
base_branch: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
worktree_dir: None,
|
||||
|
|
@ -2476,6 +2560,7 @@ mod tests {
|
|||
let event = WorkflowRunEvent::WorkflowRunStarted {
|
||||
name: "wf".to_string(),
|
||||
run_id: "r1".to_string(),
|
||||
base_branch: None,
|
||||
base_sha: None,
|
||||
run_branch: None,
|
||||
worktree_dir: None,
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ impl Handler for SubWorkflowHandler {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -193,6 +194,9 @@ impl Handler for SubWorkflowHandler {
|
|||
hook_runner,
|
||||
env,
|
||||
dry_run,
|
||||
llm_client: None,
|
||||
model: String::new(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
};
|
||||
let executed = pipeline::execute(initialized).await;
|
||||
Ok::<_, FabroError>((executed.outcome?, executed.final_context))
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ pub struct EventLifecycle {
|
|||
/// Set in on_edge_selected when loop_restart approved; emitted+cleared in on_run_start.
|
||||
pub restarted_from: Arc<Mutex<Option<(String, String)>>>,
|
||||
// Config for WorkflowRunStarted payload
|
||||
pub base_branch: Option<String>,
|
||||
pub base_sha: Option<String>,
|
||||
pub run_branch: Option<String>,
|
||||
pub worktree_dir: Option<String>,
|
||||
|
|
@ -70,6 +71,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
|
|||
self.emitter.emit(&WorkflowRunEvent::WorkflowRunStarted {
|
||||
name: self.graph_name.clone(),
|
||||
run_id: self.run_id.clone(),
|
||||
base_branch: self.base_branch.clone(),
|
||||
base_sha: self.base_sha.clone(),
|
||||
run_branch: self.run_branch.clone(),
|
||||
worktree_dir: self.worktree_dir.clone(),
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ impl WorkflowLifecycle {
|
|||
run_id: run_options.run_id.clone(),
|
||||
run_start: Mutex::new(Instant::now()),
|
||||
restarted_from: Arc::clone(&restarted_from),
|
||||
base_sha: run_options.git.as_ref().and_then(|g| g.base_sha.clone()),
|
||||
base_branch: run_options.base_branch.clone(),
|
||||
base_sha: run_options.display_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()),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod fork;
|
|||
mod rewind;
|
||||
mod start;
|
||||
|
||||
pub use crate::pipeline::{DevcontainerSpec, LlmSpec, SandboxEnvSpec, SandboxSpec};
|
||||
pub use create::{
|
||||
create, create_from_file, default_run_dir, validate, validate_from_file, RunCreateOptions,
|
||||
ValidateOptions,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,21 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::FabroError;
|
||||
use crate::event::{EventEmitter, WorkflowRunEvent};
|
||||
use crate::handler::HandlerRegistry;
|
||||
use crate::event::{EventEmitter, ProgressLogger, WorkflowRunEvent};
|
||||
use crate::outcome::StageStatus;
|
||||
use crate::pipeline::{
|
||||
self, FinalizeOptions, Finalized, InitOptions, Persisted, PullRequestOptions, RetroOptions,
|
||||
self, DevcontainerSpec, FinalizeOptions, Finalized, InitOptions, LlmSpec, Persisted,
|
||||
PullRequestOptions, RetroOptions, SandboxEnvSpec, SandboxSpec,
|
||||
};
|
||||
use crate::records::{Checkpoint, Conclusion};
|
||||
use crate::run_options::{GitCheckpointOptions, LifecycleOptions, RunOptions};
|
||||
use fabro_config::sandbox::WorktreeMode;
|
||||
use fabro_interview::Interviewer;
|
||||
|
||||
pub struct StartRetroOptions {
|
||||
pub enabled: bool,
|
||||
pub dry_run: bool,
|
||||
pub llm_client: Option<fabro_llm::client::Client>,
|
||||
pub provider: fabro_llm::Provider,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
pub struct StartFinalizeOptions {
|
||||
|
|
@ -41,15 +38,20 @@ pub struct StartOptions {
|
|||
// Truly external (not derivable from RunRecord)
|
||||
pub cancel_token: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub sandbox: Arc<dyn fabro_agent::Sandbox>,
|
||||
pub registry: Arc<HandlerRegistry>,
|
||||
pub sandbox: SandboxSpec,
|
||||
pub llm: LlmSpec,
|
||||
pub interviewer: Arc<dyn Interviewer>,
|
||||
pub lifecycle: LifecycleOptions,
|
||||
pub hooks: fabro_hooks::HookConfig,
|
||||
pub sandbox_env: HashMap<String, String>,
|
||||
pub sandbox_env: SandboxEnvSpec,
|
||||
pub devcontainer: Option<DevcontainerSpec>,
|
||||
pub seed_context: Option<Context>,
|
||||
pub git_author: crate::git::GitAuthor,
|
||||
pub git: Option<GitCheckpointOptions>,
|
||||
pub github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
pub worktree_mode: Option<WorktreeMode>,
|
||||
#[cfg(test)]
|
||||
pub registry_override: Option<Arc<crate::handler::HandlerRegistry>>,
|
||||
|
||||
// Still external for now — could be derived from RunRecord.config in follow-up
|
||||
pub dry_run: bool,
|
||||
|
|
@ -114,17 +116,6 @@ async fn run_engine(
|
|||
options: StartOptions,
|
||||
) -> Result<Started, FabroError> {
|
||||
let preserve_sandbox = options.finalize.preserve_sandbox;
|
||||
let sandbox_for_cleanup = Arc::clone(&options.sandbox);
|
||||
let cleanup_guard = scopeguard::guard((), move |()| {
|
||||
if preserve_sandbox {
|
||||
return;
|
||||
}
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
handle.spawn(async move {
|
||||
let _ = sandbox_for_cleanup.cleanup().await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Build RunOptions from the persisted RunRecord + external caller options
|
||||
let record = persisted.run_record();
|
||||
|
|
@ -143,37 +134,64 @@ async fn run_engine(
|
|||
.as_deref()
|
||||
.map(std::path::PathBuf::from),
|
||||
base_branch: record.base_branch.clone(),
|
||||
git: options.git,
|
||||
display_base_sha: None,
|
||||
git: None,
|
||||
};
|
||||
|
||||
let last_git_sha: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||
{
|
||||
let sha_clone = Arc::clone(&last_git_sha);
|
||||
options.emitter.on_event(move |event| match event {
|
||||
WorkflowRunEvent::CheckpointCompleted {
|
||||
git_commit_sha: Some(sha),
|
||||
..
|
||||
}
|
||||
| WorkflowRunEvent::WorkflowRunCompleted {
|
||||
final_git_commit_sha: Some(sha),
|
||||
..
|
||||
}
|
||||
| WorkflowRunEvent::GitCommit { sha, .. } => {
|
||||
*sha_clone.lock().unwrap() = Some(sha.clone());
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
ProgressLogger::new(persisted.run_dir(), record.run_id.clone())
|
||||
.register(options.emitter.as_ref());
|
||||
|
||||
let init_options = InitOptions {
|
||||
run_id: record.run_id.clone(),
|
||||
dry_run: options.dry_run,
|
||||
emitter: options.emitter,
|
||||
sandbox: options.sandbox,
|
||||
registry: options.registry,
|
||||
llm: options.llm,
|
||||
interviewer: options.interviewer,
|
||||
lifecycle: options.lifecycle,
|
||||
run_options,
|
||||
hooks: options.hooks,
|
||||
sandbox_env: options.sandbox_env,
|
||||
devcontainer: options.devcontainer,
|
||||
git: options.git,
|
||||
worktree_mode: options.worktree_mode,
|
||||
#[cfg(test)]
|
||||
registry_override: options.registry_override,
|
||||
checkpoint,
|
||||
seed_context: options.seed_context,
|
||||
};
|
||||
let initialized = pipeline::initialize(persisted, init_options).await?;
|
||||
|
||||
let last_git_sha: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||
{
|
||||
let sha_clone = Arc::clone(&last_git_sha);
|
||||
initialized.emitter.on_event(move |event| {
|
||||
if let WorkflowRunEvent::CheckpointCompleted {
|
||||
git_commit_sha: Some(sha),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
*sha_clone.lock().unwrap() = Some(sha.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
let sandbox_for_cleanup = Arc::clone(&initialized.sandbox);
|
||||
let cleanup_guard = scopeguard::guard((), move |()| {
|
||||
if preserve_sandbox {
|
||||
return;
|
||||
}
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
handle.spawn(async move {
|
||||
let _ = sandbox_for_cleanup.cleanup().await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let executed = pipeline::execute(initialized).await;
|
||||
let failed = !matches!(
|
||||
|
|
@ -191,10 +209,9 @@ async fn run_engine(
|
|||
failed,
|
||||
run_duration_ms: executed.duration_ms,
|
||||
enabled: options.retro.enabled,
|
||||
dry_run: options.retro.dry_run,
|
||||
llm_client: options.retro.llm_client,
|
||||
provider: options.retro.provider,
|
||||
model: options.retro.model,
|
||||
llm_client: executed.llm_client.clone(),
|
||||
provider: executed.provider,
|
||||
model: executed.model.clone(),
|
||||
};
|
||||
|
||||
let retro_start = Instant::now();
|
||||
|
|
@ -233,23 +250,19 @@ async fn run_engine(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use fabro_agent::{DirEntry, ExecResult, GrepOptions, LocalSandbox, Sandbox};
|
||||
use fabro_agent::{LocalSandbox, Sandbox};
|
||||
use fabro_config::config::FabroConfig;
|
||||
use fabro_graphviz::graph::{Graph, Node};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::*;
|
||||
use crate::context::Context;
|
||||
use crate::event::EventEmitter;
|
||||
use crate::handler::exit::ExitHandler;
|
||||
use crate::handler::start::StartHandler;
|
||||
use crate::handler::{Handler, HandlerRegistry};
|
||||
use crate::outcome::Outcome;
|
||||
use crate::handler::HandlerRegistry;
|
||||
use crate::pipeline::{LlmSpec, SandboxEnvSpec, SandboxSpec};
|
||||
use crate::run_options::LifecycleOptions;
|
||||
|
||||
const MINIMAL_DOT: &str = r#"digraph Test {
|
||||
|
|
@ -259,202 +272,6 @@ mod tests {
|
|||
start -> exit
|
||||
}"#;
|
||||
|
||||
const EMIT_DOT: &str = r#"digraph Test {
|
||||
graph [goal="Ship feature"]
|
||||
start [shape=Mdiamond]
|
||||
work [type="emit"]
|
||||
exit [shape=Msquare]
|
||||
start -> work -> exit
|
||||
}"#;
|
||||
|
||||
struct CleanupCountingSandbox {
|
||||
inner: Arc<dyn Sandbox>,
|
||||
cleanup_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Sandbox for CleanupCountingSandbox {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
offset: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
) -> Result<String, String> {
|
||||
self.inner.read_file(path, offset, limit).await
|
||||
}
|
||||
|
||||
async fn write_file(&self, path: &str, content: &str) -> Result<(), String> {
|
||||
self.inner.write_file(path, content).await
|
||||
}
|
||||
|
||||
async fn delete_file(&self, path: &str) -> Result<(), String> {
|
||||
self.inner.delete_file(path).await
|
||||
}
|
||||
|
||||
async fn file_exists(&self, path: &str) -> Result<bool, String> {
|
||||
self.inner.file_exists(path).await
|
||||
}
|
||||
|
||||
async fn list_directory(
|
||||
&self,
|
||||
path: &str,
|
||||
depth: Option<usize>,
|
||||
) -> Result<Vec<DirEntry>, String> {
|
||||
self.inner.list_directory(path, depth).await
|
||||
}
|
||||
|
||||
async fn exec_command(
|
||||
&self,
|
||||
command: &str,
|
||||
timeout_ms: u64,
|
||||
working_dir: Option<&str>,
|
||||
env_vars: Option<&HashMap<String, String>>,
|
||||
cancel_token: Option<CancellationToken>,
|
||||
) -> Result<ExecResult, String> {
|
||||
self.inner
|
||||
.exec_command(command, timeout_ms, working_dir, env_vars, cancel_token)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn grep(
|
||||
&self,
|
||||
pattern: &str,
|
||||
path: &str,
|
||||
options: &GrepOptions,
|
||||
) -> Result<Vec<String>, String> {
|
||||
self.inner.grep(pattern, path, options).await
|
||||
}
|
||||
|
||||
async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>, String> {
|
||||
self.inner.glob(pattern, path).await
|
||||
}
|
||||
|
||||
async fn download_file_to_local(
|
||||
&self,
|
||||
remote_path: &str,
|
||||
local_path: &Path,
|
||||
) -> Result<(), String> {
|
||||
self.inner
|
||||
.download_file_to_local(remote_path, local_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn upload_file_from_local(
|
||||
&self,
|
||||
local_path: &Path,
|
||||
remote_path: &str,
|
||||
) -> Result<(), String> {
|
||||
self.inner
|
||||
.upload_file_from_local(local_path, remote_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn initialize(&self) -> Result<(), String> {
|
||||
self.inner.initialize().await
|
||||
}
|
||||
|
||||
async fn cleanup(&self) -> Result<(), String> {
|
||||
self.cleanup_count.fetch_add(1, Ordering::SeqCst);
|
||||
self.inner.cleanup().await
|
||||
}
|
||||
|
||||
fn working_directory(&self) -> &str {
|
||||
self.inner.working_directory()
|
||||
}
|
||||
|
||||
fn platform(&self) -> &str {
|
||||
self.inner.platform()
|
||||
}
|
||||
|
||||
fn os_version(&self) -> String {
|
||||
self.inner.os_version()
|
||||
}
|
||||
|
||||
fn sandbox_info(&self) -> String {
|
||||
self.inner.sandbox_info()
|
||||
}
|
||||
|
||||
async fn refresh_push_credentials(&self) -> Result<(), String> {
|
||||
self.inner.refresh_push_credentials().await
|
||||
}
|
||||
|
||||
async fn set_autostop_interval(&self, minutes: i32) -> Result<(), String> {
|
||||
self.inner.set_autostop_interval(minutes).await
|
||||
}
|
||||
|
||||
async fn setup_git_for_run(
|
||||
&self,
|
||||
run_id: &str,
|
||||
) -> Result<Option<fabro_sandbox::GitRunInfo>, String> {
|
||||
self.inner.setup_git_for_run(run_id).await
|
||||
}
|
||||
|
||||
fn resume_setup_commands(&self, run_branch: &str) -> Vec<String> {
|
||||
self.inner.resume_setup_commands(run_branch)
|
||||
}
|
||||
|
||||
async fn git_push_branch(&self, branch: &str) -> bool {
|
||||
self.inner.git_push_branch(branch).await
|
||||
}
|
||||
|
||||
fn host_git_dir(&self) -> Option<&str> {
|
||||
self.inner.host_git_dir()
|
||||
}
|
||||
|
||||
fn parallel_worktree_path(
|
||||
&self,
|
||||
run_dir: &Path,
|
||||
run_id: &str,
|
||||
node_id: &str,
|
||||
key: &str,
|
||||
) -> String {
|
||||
self.inner
|
||||
.parallel_worktree_path(run_dir, run_id, node_id, key)
|
||||
}
|
||||
|
||||
async fn ssh_access_command(&self) -> Result<Option<String>, String> {
|
||||
self.inner.ssh_access_command().await
|
||||
}
|
||||
|
||||
fn origin_url(&self) -> Option<&str> {
|
||||
self.inner.origin_url()
|
||||
}
|
||||
|
||||
async fn get_preview_url(
|
||||
&self,
|
||||
port: u16,
|
||||
) -> Result<Option<(String, HashMap<String, String>)>, String> {
|
||||
self.inner.get_preview_url(port).await
|
||||
}
|
||||
|
||||
fn mark_agent_read(&self, path: &str) {
|
||||
self.inner.mark_agent_read(path);
|
||||
}
|
||||
}
|
||||
|
||||
struct EmitCheckpointHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl Handler for EmitCheckpointHandler {
|
||||
async fn execute(
|
||||
&self,
|
||||
node: &Node,
|
||||
_context: &Context,
|
||||
_graph: &Graph,
|
||||
_run_dir: &Path,
|
||||
services: &crate::handler::EngineServices,
|
||||
) -> Result<Outcome, FabroError> {
|
||||
services
|
||||
.emitter
|
||||
.emit(&WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: node.id.clone(),
|
||||
status: "success".to_string(),
|
||||
git_commit_sha: Some("sha-test".to_string()),
|
||||
});
|
||||
Ok(Outcome::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn persisted_workflow(dot: &str, run_dir: &std::path::Path) -> Persisted {
|
||||
crate::operations::create(
|
||||
dot,
|
||||
|
|
@ -478,13 +295,12 @@ mod tests {
|
|||
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
registry.register("emit", Box::new(EmitCheckpointHandler));
|
||||
registry
|
||||
}
|
||||
|
||||
fn test_start_options(
|
||||
_run_dir: &std::path::Path,
|
||||
sandbox: Arc<dyn Sandbox>,
|
||||
_sandbox: Arc<dyn Sandbox>,
|
||||
emitter: Arc<EventEmitter>,
|
||||
registry: Arc<HandlerRegistry>,
|
||||
lifecycle: LifecycleOptions,
|
||||
|
|
@ -493,23 +309,34 @@ mod tests {
|
|||
StartOptions {
|
||||
cancel_token: None,
|
||||
emitter,
|
||||
sandbox,
|
||||
registry,
|
||||
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(fabro_interview::AutoApproveInterviewer),
|
||||
lifecycle,
|
||||
hooks: fabro_hooks::HookConfig { hooks: vec![] },
|
||||
sandbox_env: HashMap::new(),
|
||||
sandbox_env: SandboxEnvSpec {
|
||||
devcontainer_env: HashMap::new(),
|
||||
toml_env: HashMap::new(),
|
||||
github_permissions: None,
|
||||
origin_url: None,
|
||||
},
|
||||
devcontainer: None,
|
||||
seed_context: None,
|
||||
git_author: crate::git::GitAuthor::default(),
|
||||
git: None,
|
||||
github_app: None,
|
||||
worktree_mode: None,
|
||||
registry_override: Some(registry),
|
||||
dry_run: false,
|
||||
retro: StartRetroOptions {
|
||||
enabled: false,
|
||||
dry_run: false,
|
||||
llm_client: None,
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
model: "test-model".to_string(),
|
||||
},
|
||||
retro: StartRetroOptions { enabled: false },
|
||||
finalize: StartFinalizeOptions { preserve_sandbox },
|
||||
pull_request: StartPullRequestConfig {
|
||||
pr_config: None,
|
||||
|
|
@ -520,25 +347,14 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn counting_sandbox() -> (Arc<dyn Sandbox>, Arc<AtomicUsize>) {
|
||||
let cleanup_count = Arc::new(AtomicUsize::new(0));
|
||||
let inner: Arc<dyn Sandbox> = Arc::new(LocalSandbox::new(std::env::current_dir().unwrap()));
|
||||
(
|
||||
Arc::new(CleanupCountingSandbox {
|
||||
inner,
|
||||
cleanup_count: Arc::clone(&cleanup_count),
|
||||
}),
|
||||
cleanup_count,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_cleans_up_sandbox_when_initialize_fails() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let run_dir = temp.path().join("run");
|
||||
let emitter = Arc::new(EventEmitter::new());
|
||||
let registry = Arc::new(test_registry());
|
||||
let (sandbox, cleanup_count) = counting_sandbox();
|
||||
let sandbox: Arc<dyn Sandbox> =
|
||||
Arc::new(LocalSandbox::new(std::env::current_dir().unwrap()));
|
||||
|
||||
persisted_workflow(MINIMAL_DOT, &run_dir);
|
||||
let result = start(
|
||||
|
|
@ -559,9 +375,6 @@ mod tests {
|
|||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
tokio::task::yield_now().await;
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
assert_eq!(cleanup_count.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -572,8 +385,29 @@ mod tests {
|
|||
let registry = Arc::new(test_registry());
|
||||
let sandbox: Arc<dyn Sandbox> =
|
||||
Arc::new(LocalSandbox::new(std::env::current_dir().unwrap()));
|
||||
let injected = Arc::new(AtomicBool::new(false));
|
||||
|
||||
persisted_workflow(EMIT_DOT, &run_dir);
|
||||
{
|
||||
let injected = Arc::clone(&injected);
|
||||
let emitter_for_injection = Arc::clone(&emitter);
|
||||
emitter.on_event(move |event| {
|
||||
if injected.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if let WorkflowRunEvent::StageStarted { node_id, .. } = event {
|
||||
if node_id == "start" {
|
||||
injected.store(true, Ordering::SeqCst);
|
||||
emitter_for_injection.emit(&WorkflowRunEvent::CheckpointCompleted {
|
||||
node_id: node_id.clone(),
|
||||
status: "success".to_string(),
|
||||
git_commit_sha: Some("sha-test".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
persisted_workflow(MINIMAL_DOT, &run_dir);
|
||||
let started = start(
|
||||
&run_dir,
|
||||
test_start_options(
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
hook_runner,
|
||||
env,
|
||||
dry_run,
|
||||
llm_client,
|
||||
model,
|
||||
provider,
|
||||
} = init;
|
||||
|
||||
let start = Instant::now();
|
||||
|
|
@ -140,6 +143,9 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
sandbox,
|
||||
duration_ms: crate::millis_u64(start.elapsed()),
|
||||
final_context: seed_context_from_checkpoint(checkpoint.as_ref()),
|
||||
llm_client,
|
||||
model,
|
||||
provider,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -161,6 +167,9 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
sandbox,
|
||||
duration_ms: crate::millis_u64(start.elapsed()),
|
||||
final_context: seed,
|
||||
llm_client,
|
||||
model,
|
||||
provider,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -177,6 +186,9 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
sandbox,
|
||||
duration_ms: crate::millis_u64(start.elapsed()),
|
||||
final_context: Context::new(),
|
||||
llm_client,
|
||||
model,
|
||||
provider,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -296,6 +308,9 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
sandbox,
|
||||
duration_ms,
|
||||
final_context,
|
||||
llm_client,
|
||||
model,
|
||||
provider,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ use super::*;
|
|||
use crate::context::{self, Context};
|
||||
use crate::error::FabroError;
|
||||
use crate::event::{EventEmitter, WorkflowRunEvent};
|
||||
use crate::handler::default_registry;
|
||||
use crate::handler::start::StartHandler;
|
||||
use crate::handler::{Handler as HandlerTrait, HandlerRegistry};
|
||||
use crate::outcome::{Outcome, OutcomeExt, StageStatus};
|
||||
use crate::pipeline::initialize;
|
||||
use crate::pipeline::types::{InitOptions, Persisted};
|
||||
use crate::pipeline::types::{
|
||||
InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec, SandboxSpec,
|
||||
};
|
||||
use crate::records::{Checkpoint, RunRecord};
|
||||
use crate::run_options::{GitCheckpointOptions, LifecycleOptions, RunOptions};
|
||||
use crate::test_support::run_graph;
|
||||
|
|
@ -79,6 +80,7 @@ fn test_run_options(run_dir: &Path, run_id: &str) -> RunOptions {
|
|||
github_app: None,
|
||||
git_author: crate::git::GitAuthor::default(),
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
workflow_slug: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -151,10 +153,17 @@ async fn execute_runs_start_to_exit_and_returns_final_context() {
|
|||
run_id: "run-test".to_string(),
|
||||
dry_run: false,
|
||||
emitter: Arc::new(crate::event::EventEmitter::new()),
|
||||
sandbox: Arc::new(fabro_agent::LocalSandbox::new(
|
||||
std::env::current_dir().unwrap(),
|
||||
)),
|
||||
registry: Arc::new(default_registry(Arc::new(AutoApproveInterviewer), || None)),
|
||||
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: LifecycleOptions {
|
||||
setup_commands: vec![],
|
||||
setup_command_timeout_ms: 1_000,
|
||||
|
|
@ -162,7 +171,16 @@ async fn execute_runs_start_to_exit_and_returns_final_context() {
|
|||
},
|
||||
run_options: test_run_options(&run_dir, "run-test"),
|
||||
hooks: HookConfig { hooks: vec![] },
|
||||
sandbox_env: HashMap::new(),
|
||||
sandbox_env: SandboxEnvSpec {
|
||||
devcontainer_env: HashMap::new(),
|
||||
toml_env: HashMap::new(),
|
||||
github_permissions: None,
|
||||
origin_url: None,
|
||||
},
|
||||
devcontainer: None,
|
||||
git: None,
|
||||
worktree_mode: None,
|
||||
registry_override: None,
|
||||
checkpoint: None,
|
||||
seed_context: None,
|
||||
},
|
||||
|
|
@ -190,28 +208,25 @@ async fn run_with_lifecycle(
|
|||
sandbox: Arc<dyn Sandbox>,
|
||||
graph: &Graph,
|
||||
run_options: RunOptions,
|
||||
lifecycle: LifecycleOptions,
|
||||
_lifecycle: LifecycleOptions,
|
||||
) -> Result<Outcome, FabroError> {
|
||||
let run_dir = run_options.run_dir.clone();
|
||||
let run_id = run_options.run_id.clone();
|
||||
std::fs::create_dir_all(&run_dir).unwrap();
|
||||
let initialized = initialize(
|
||||
persisted_workflow(graph.clone(), String::new(), &run_dir, &run_id),
|
||||
InitOptions {
|
||||
run_id,
|
||||
dry_run: run_options.dry_run,
|
||||
emitter,
|
||||
sandbox,
|
||||
registry: Arc::new(registry),
|
||||
lifecycle,
|
||||
run_options,
|
||||
hooks: HookConfig { hooks: vec![] },
|
||||
sandbox_env: HashMap::new(),
|
||||
checkpoint: None,
|
||||
seed_context: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
std::fs::create_dir_all(&run_options.run_dir).unwrap();
|
||||
let initialized = Initialized {
|
||||
graph: graph.clone(),
|
||||
source: String::new(),
|
||||
run_options,
|
||||
checkpoint: None,
|
||||
seed_context: None,
|
||||
emitter,
|
||||
sandbox,
|
||||
registry: Arc::new(registry),
|
||||
hook_runner: None,
|
||||
env: HashMap::new(),
|
||||
dry_run: false,
|
||||
llm_client: None,
|
||||
model: "test-model".to_string(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
};
|
||||
super::execute(initialized).await.outcome
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ mod tests {
|
|||
github_app: None,
|
||||
host_repo_path: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
git: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -23,7 +23,8 @@ pub use pull_request::{
|
|||
pub use retro::{retro, run_retro};
|
||||
pub use transform::transform;
|
||||
pub use types::{
|
||||
Concluded, Executed, FinalizeOptions, Finalized, InitOptions, Initialized, Parsed, Persisted,
|
||||
PullRequestOptions, RetroOptions, Retroed, TransformOptions, Transformed, Validated,
|
||||
Concluded, DevcontainerSpec, Executed, FinalizeOptions, Finalized, InitOptions, Initialized,
|
||||
LlmSpec, Parsed, Persisted, PullRequestOptions, RetroOptions, Retroed, SandboxEnvSpec,
|
||||
SandboxSpec, TransformOptions, Transformed, Validated,
|
||||
};
|
||||
pub use validate::validate;
|
||||
|
|
|
|||
|
|
@ -488,7 +488,9 @@ pub async fn pull_request(concluded: Concluded, options: &PullRequestOptions) ->
|
|||
|
||||
let mut pr_url = None;
|
||||
if let Some(pr_cfg) = &options.pr_config {
|
||||
if let Err(ref e) = outcome {
|
||||
if run_options.dry_run {
|
||||
tracing::debug!("Skipping PR creation: run is in dry-run mode");
|
||||
} else if let Err(ref e) = outcome {
|
||||
tracing::debug!(error = %e, "Skipping PR creation: engine returned an error");
|
||||
} else if let Ok(ref result) = outcome {
|
||||
if matches!(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::records::Checkpoint;
|
|||
|
||||
use super::types::{Executed, RetroOptions, Retroed};
|
||||
|
||||
pub async fn run_retro(options: &RetroOptions) -> Option<Retro> {
|
||||
pub async fn run_retro(options: &RetroOptions, dry_run: bool) -> Option<Retro> {
|
||||
let cp = match Checkpoint::load(&options.run_dir.join("checkpoint.json")) {
|
||||
Ok(cp) => cp,
|
||||
Err(e) => {
|
||||
|
|
@ -43,7 +43,7 @@ pub async fn run_retro(options: &RetroOptions) -> Option<Retro> {
|
|||
emitter.emit(&WorkflowRunEvent::RetroStarted);
|
||||
}
|
||||
|
||||
let narrative_result = if options.dry_run {
|
||||
let narrative_result = if dry_run {
|
||||
Ok(fabro_retro::retro_agent::dry_run_narrative())
|
||||
} else if let Some(client) = options.llm_client.as_ref() {
|
||||
let emitter_clone = options.emitter.clone();
|
||||
|
|
@ -122,10 +122,15 @@ pub async fn retro(executed: Executed, options: &RetroOptions) -> Retroed {
|
|||
sandbox,
|
||||
duration_ms,
|
||||
final_context: _,
|
||||
llm_client: _,
|
||||
model: _,
|
||||
provider: _,
|
||||
} = executed;
|
||||
|
||||
let dry_run = run_options.dry_run;
|
||||
|
||||
let retro = if options.enabled {
|
||||
run_retro(options).await
|
||||
run_retro(options, dry_run).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -189,6 +194,7 @@ mod tests {
|
|||
github_app: None,
|
||||
host_repo_path: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
git: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -213,6 +219,9 @@ mod tests {
|
|||
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(
|
||||
|
|
@ -227,7 +236,6 @@ mod tests {
|
|||
failed: false,
|
||||
run_duration_ms: 1,
|
||||
enabled: true,
|
||||
dry_run: true,
|
||||
llm_client: None,
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
model: "test-model".to_string(),
|
||||
|
|
@ -253,23 +261,25 @@ mod tests {
|
|||
move |event| seen.lock().unwrap().push(event.clone())
|
||||
});
|
||||
|
||||
let retro = run_retro(&RetroOptions {
|
||||
run_id: "run-test".to_string(),
|
||||
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,
|
||||
run_duration_ms: 1,
|
||||
enabled: true,
|
||||
dry_run: true,
|
||||
llm_client: None,
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
model: "test-model".to_string(),
|
||||
})
|
||||
let retro = run_retro(
|
||||
&RetroOptions {
|
||||
run_id: "run-test".to_string(),
|
||||
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,
|
||||
run_duration_ms: 1,
|
||||
enabled: true,
|
||||
llm_client: None,
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
model: "test-model".to_string(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(retro.is_some());
|
||||
|
|
|
|||
|
|
@ -3,14 +3,23 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_config::sandbox::WorktreeMode;
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_hooks::HookRunner;
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_llm::Provider;
|
||||
use fabro_mcp::config::McpServerConfig;
|
||||
use fabro_model::FallbackTarget;
|
||||
use fabro_sandbox::daytona::DaytonaConfig;
|
||||
use fabro_sandbox::docker::DockerSandboxConfig;
|
||||
#[cfg(feature = "exedev")]
|
||||
use fabro_sandbox::exe::{ExeConfig, GitCloneParams as ExeGitCloneParams};
|
||||
use fabro_sandbox::ssh::{GitCloneParams as SshGitCloneParams, SshConfig};
|
||||
use fabro_validate::Diagnostic;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::FabroError;
|
||||
use crate::event::EventEmitter;
|
||||
use crate::handler::HandlerRegistry;
|
||||
use crate::outcome::Outcome;
|
||||
use crate::records::{Checkpoint, Conclusion, RunRecord};
|
||||
use crate::run_options::{LifecycleOptions, RunOptions};
|
||||
|
|
@ -189,16 +198,74 @@ impl Persisted {
|
|||
}
|
||||
|
||||
/// Options for the INITIALIZE phase.
|
||||
pub enum SandboxSpec {
|
||||
Local {
|
||||
working_directory: PathBuf,
|
||||
},
|
||||
Docker {
|
||||
config: DockerSandboxConfig,
|
||||
},
|
||||
Daytona {
|
||||
config: DaytonaConfig,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
run_id: Option<String>,
|
||||
clone_branch: Option<String>,
|
||||
},
|
||||
#[cfg(feature = "exedev")]
|
||||
Exe {
|
||||
config: ExeConfig,
|
||||
clone_params: Option<ExeGitCloneParams>,
|
||||
run_id: Option<String>,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
mgmt_destination: String,
|
||||
},
|
||||
Ssh {
|
||||
config: SshConfig,
|
||||
clone_params: Option<SshGitCloneParams>,
|
||||
run_id: Option<String>,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LlmSpec {
|
||||
pub model: String,
|
||||
pub provider: Provider,
|
||||
pub fallback_chain: Vec<FallbackTarget>,
|
||||
pub mcp_servers: Vec<McpServerConfig>,
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SandboxEnvSpec {
|
||||
pub devcontainer_env: HashMap<String, String>,
|
||||
pub toml_env: HashMap<String, String>,
|
||||
pub github_permissions: Option<HashMap<String, String>>,
|
||||
pub origin_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DevcontainerSpec {
|
||||
pub enabled: bool,
|
||||
pub resolve_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub struct InitOptions {
|
||||
pub run_id: String,
|
||||
pub dry_run: bool,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
pub registry: Arc<HandlerRegistry>,
|
||||
pub sandbox: SandboxSpec,
|
||||
pub llm: LlmSpec,
|
||||
pub interviewer: Arc<dyn Interviewer>,
|
||||
pub lifecycle: LifecycleOptions,
|
||||
pub run_options: RunOptions,
|
||||
pub hooks: fabro_hooks::HookConfig,
|
||||
pub sandbox_env: HashMap<String, String>,
|
||||
pub sandbox_env: SandboxEnvSpec,
|
||||
pub devcontainer: Option<DevcontainerSpec>,
|
||||
pub git: Option<crate::run_options::GitCheckpointOptions>,
|
||||
pub worktree_mode: Option<WorktreeMode>,
|
||||
#[cfg(test)]
|
||||
pub registry_override: Option<Arc<crate::handler::HandlerRegistry>>,
|
||||
pub checkpoint: Option<Checkpoint>,
|
||||
pub seed_context: Option<Context>,
|
||||
}
|
||||
|
|
@ -213,10 +280,13 @@ pub struct Initialized {
|
|||
pub(crate) seed_context: Option<Context>,
|
||||
pub emitter: Arc<EventEmitter>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
pub registry: Arc<HandlerRegistry>,
|
||||
pub registry: Arc<crate::handler::HandlerRegistry>,
|
||||
pub hook_runner: Option<Arc<HookRunner>>,
|
||||
pub env: HashMap<String, String>,
|
||||
pub dry_run: bool,
|
||||
pub llm_client: Option<fabro_llm::client::Client>,
|
||||
pub model: String,
|
||||
pub provider: Provider,
|
||||
}
|
||||
|
||||
/// Output of the EXECUTE phase.
|
||||
|
|
@ -230,6 +300,9 @@ pub struct Executed {
|
|||
pub sandbox: Arc<dyn Sandbox>,
|
||||
pub duration_ms: u64,
|
||||
pub final_context: Context,
|
||||
pub llm_client: Option<fabro_llm::client::Client>,
|
||||
pub model: String,
|
||||
pub provider: Provider,
|
||||
}
|
||||
|
||||
/// Output of the RETRO phase.
|
||||
|
|
@ -284,9 +357,8 @@ pub struct RetroOptions {
|
|||
pub failed: bool,
|
||||
pub run_duration_ms: u64,
|
||||
pub enabled: bool,
|
||||
pub dry_run: bool,
|
||||
pub llm_client: Option<fabro_llm::client::Client>,
|
||||
pub provider: fabro_llm::Provider,
|
||||
pub provider: Provider,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ pub struct RunOptions {
|
|||
pub host_repo_path: Option<PathBuf>,
|
||||
/// Name of the branch the run was started from (for PR base).
|
||||
pub base_branch: Option<String>,
|
||||
/// Base commit SHA to display in lifecycle events/UI even when checkpointing is disabled.
|
||||
pub display_base_sha: Option<String>,
|
||||
/// Git checkpoint options; `None` means checkpointing disabled.
|
||||
pub git: Option<GitCheckpointOptions>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ fn initialized(
|
|||
hook_runner: options.hook_runner,
|
||||
env: options.env,
|
||||
dry_run: run_options.dry_run,
|
||||
llm_client: None,
|
||||
model: String::new(),
|
||||
provider: fabro_llm::Provider::Anthropic,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -399,6 +399,7 @@ async fn daytona_pipeline_artifact_offload_and_sync() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -590,6 +591,7 @@ async fn daytona_git_checkpoint_remote_emits_events() {
|
|||
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),
|
||||
|
|
@ -776,6 +778,7 @@ async fn daytona_parallel_git_branching_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: Some(run_tmp.path().to_path_buf()),
|
||||
git: Some(GitCheckpointOptions {
|
||||
base_sha: Some(base_sha),
|
||||
|
|
@ -1152,6 +1155,7 @@ async fn daytona_git_checkpoint_with_shadow_branch() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: Some(host_repo.path().to_path_buf()),
|
||||
git: Some(GitCheckpointOptions {
|
||||
base_sha: Some(base_sha),
|
||||
|
|
@ -1297,6 +1301,7 @@ async fn daytona_asset_collection() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -1548,6 +1553,7 @@ async fn daytona_git_push_run_branch_to_origin() {
|
|||
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),
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ async fn end_to_end_linear_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -343,6 +344,7 @@ async fn end_to_end_branching_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -463,6 +465,7 @@ async fn end_to_end_human_gate_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -559,6 +562,7 @@ async fn human_gate_aborted_input_fails_closed_without_fail_route() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -670,6 +674,7 @@ async fn human_gate_aborted_input_routes_via_outcome_fail_condition() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -783,6 +788,7 @@ async fn goal_gate_routes_to_retry_target_on_failure() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -904,6 +910,7 @@ async fn goal_gate_routes_to_retry_target_when_present() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -1216,6 +1223,7 @@ async fn retry_on_failure_then_succeed() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -1291,6 +1299,7 @@ async fn pipeline_with_many_nodes() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -1613,6 +1622,7 @@ async fn smoke_test_with_mock_codergen_backend() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -1714,6 +1724,7 @@ async fn end_to_end_parallel_fan_out_fan_in() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -1827,6 +1838,7 @@ async fn resume_from_checkpoint_completes_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -1926,6 +1938,7 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -1969,6 +1982,7 @@ async fn graph_goal_in_context() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2005,6 +2019,7 @@ async fn event_streaming_lifecycle() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2085,6 +2100,7 @@ async fn context_flow_between_stages() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2138,6 +2154,7 @@ async fn tool_handler_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2208,6 +2225,7 @@ async fn auto_approve_interviewer_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2245,6 +2263,7 @@ async fn codergen_without_backend_simulated() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2350,6 +2369,7 @@ async fn branching_loop_back_on_failure() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2433,6 +2453,7 @@ async fn human_gate_loops_back() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2491,6 +2512,7 @@ async fn scenario_ship_a_feature() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2577,6 +2599,7 @@ async fn scenario_parallel_expert_review() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2661,6 +2684,7 @@ async fn scenario_node_retries_on_retry_status() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2723,6 +2747,7 @@ async fn scenario_loop_restart_resets_context() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2791,6 +2816,7 @@ async fn scenario_bug_triage_router() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2850,6 +2876,7 @@ async fn scenario_crash_recovery() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -2959,6 +2986,7 @@ async fn manager_loop_stop_condition_satisfied_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3036,6 +3064,7 @@ async fn manager_loop_max_cycles_exceeded_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3172,6 +3201,7 @@ async fn conditional_branching_success_fail_paths() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3225,6 +3255,7 @@ async fn edge_selection_condition_match_wins_over_weight() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3272,6 +3303,7 @@ async fn edge_selection_weight_breaks_ties() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3311,6 +3343,7 @@ async fn edge_selection_lexical_tiebreak() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3369,6 +3402,7 @@ async fn context_updates_visible_across_nodes() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3413,6 +3447,7 @@ async fn stylesheet_applies_model_override() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3469,6 +3504,7 @@ async fn custom_handler_registration_and_execution() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3540,6 +3576,7 @@ async fn integration_smoke_plan_implement_review_done() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3644,6 +3681,7 @@ async fn manager_loop_runs_child_engine_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3778,6 +3816,7 @@ async fn manager_loop_context_flows_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3851,6 +3890,7 @@ async fn manager_loop_child_dotfile_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -3964,6 +4004,7 @@ async fn graph_merge_e2e_through_engine() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4114,6 +4155,7 @@ async fn fidelity_default_is_compact() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4171,6 +4213,7 @@ async fn fidelity_graph_default_applied() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4224,6 +4267,7 @@ async fn fidelity_node_overrides_graph_default() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4283,6 +4327,7 @@ async fn fidelity_edge_overrides_node_and_graph() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4332,6 +4377,7 @@ async fn fidelity_full_produces_empty_preamble() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4391,6 +4437,7 @@ async fn fidelity_truncate_preamble_minimal() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4463,6 +4510,7 @@ async fn fidelity_summary_low_mode() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4530,6 +4578,7 @@ async fn fidelity_summary_medium_mode() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4597,6 +4646,7 @@ async fn fidelity_summary_high_mode() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4657,6 +4707,7 @@ async fn fidelity_full_sets_thread_id_in_context() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4728,6 +4779,7 @@ async fn fidelity_full_nodes_share_thread_id() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4809,6 +4861,7 @@ async fn fidelity_resume_degrades_full_to_summary_high() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4906,6 +4959,7 @@ async fn fidelity_resume_degrade_only_affects_first_hop() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -4990,6 +5044,7 @@ async fn fidelity_resume_no_degrade_when_not_full() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5032,6 +5087,7 @@ async fn fidelity_stored_in_checkpoint_context() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5118,6 +5174,7 @@ async fn fidelity_precedence_multi_node_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5186,6 +5243,7 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5261,6 +5319,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5328,6 +5387,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5399,6 +5459,7 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5453,6 +5514,7 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5510,6 +5572,7 @@ async fn fidelity_edge_thread_id_override_in_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5568,6 +5631,7 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5636,6 +5700,7 @@ async fn fidelity_from_parsed_dot_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5684,6 +5749,7 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5754,6 +5820,7 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -5841,6 +5908,7 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6056,6 +6124,7 @@ mod real_llm {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6170,6 +6239,7 @@ mod real_llm {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6309,6 +6379,7 @@ mod real_llm {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6416,6 +6487,7 @@ mod real_llm {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6512,6 +6584,7 @@ async fn human_gate_freeform_only_routes_text() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6642,6 +6715,7 @@ async fn human_gate_freeform_with_fixed_choice_match() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6757,6 +6831,7 @@ async fn human_gate_freeform_fallback_on_unmatched_text() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6885,6 +6960,7 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -6993,6 +7069,7 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -7274,6 +7351,7 @@ fn make_run_options(dir: &std::path::Path) -> RunOptions {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
}
|
||||
|
|
@ -8378,6 +8456,7 @@ async fn arc_e2e_with_real_llm() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -8506,6 +8585,7 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -8705,6 +8785,7 @@ async fn large_context_values_are_offloaded_to_artifact_store() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -8924,6 +9005,7 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -9054,6 +9136,7 @@ async fn node_dir_uses_visit_count_on_revisit() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -10026,6 +10109,7 @@ async fn full_pipeline_with_cli_backend_node() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -10157,6 +10241,7 @@ async fn stylesheet_backend_property_routes_to_cli() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -10436,6 +10521,7 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() {
|
|||
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()),
|
||||
|
|
@ -10639,6 +10725,7 @@ async fn git_checkpoint_host_writes_shadow_branch() {
|
|||
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),
|
||||
|
|
@ -10837,6 +10924,7 @@ async fn parallel_git_branching_host_e2e() {
|
|||
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()),
|
||||
|
|
@ -11100,6 +11188,7 @@ async fn git_checkpoint_host_skips_empty_diff_patch() {
|
|||
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()),
|
||||
|
|
@ -11482,6 +11571,7 @@ async fn e2e_circuit_breaker_deterministic_self_loop() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -11529,6 +11619,7 @@ async fn e2e_circuit_breaker_custom_limit() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -11569,6 +11660,7 @@ async fn e2e_circuit_breaker_ignores_transient_failures() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -11616,6 +11708,7 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -11656,6 +11749,7 @@ async fn e2e_circuit_breaker_loop_restart() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -11718,6 +11812,7 @@ async fn e2e_failure_signature_persisted_in_context() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -11782,6 +11877,7 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -11838,6 +11934,7 @@ async fn e2e_signature_maps_persist_in_checkpoint() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -11965,6 +12062,7 @@ async fn e2e_circuit_breaker_emits_events_before_abort() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12032,6 +12130,7 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12128,6 +12227,7 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12225,6 +12325,7 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12265,6 +12366,7 @@ async fn e2e_loop_restart_blocked_for_structural_failure() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12305,6 +12407,7 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12345,6 +12448,7 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12382,6 +12486,7 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12423,6 +12528,7 @@ async fn e2e_loop_restart_allowed_for_transient_infra() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12527,6 +12633,7 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12583,6 +12690,7 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12629,6 +12737,7 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12694,6 +12803,7 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12830,6 +12940,7 @@ async fn asset_collection_local_sandbox_success() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -12944,6 +13055,7 @@ async fn asset_collection_local_sandbox_on_failure() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -13041,6 +13153,7 @@ async fn asset_collection_docker_sandbox() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
@ -13110,6 +13223,7 @@ async fn wait_timer_e2e() {
|
|||
workflow_slug: None,
|
||||
github_app: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
host_repo_path: None,
|
||||
git: None,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue