diff --git a/docs/api-reference/fabro-api.yaml b/docs/api-reference/fabro-api.yaml index 6cda7ad03..59d9b2dd7 100644 --- a/docs/api-reference/fabro-api.yaml +++ b/docs/api-reference/fabro-api.yaml @@ -4259,9 +4259,9 @@ components: description: Structured server configuration mirroring FabroConfig. type: object properties: - data_dir: + storage_dir: type: string - description: Data directory path. + description: Storage directory path. max_concurrent_runs: type: integer description: Maximum concurrent runs. diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index e5b83bbe2..9b8210e58 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -70,7 +70,7 @@ fabro run run.toml | Argument / Flag | Description | |---|---| | `` | Path to a `.fabro` workflow file, `.toml` task config, or workflow name (resolved from `fabro/workflows/` in the project, then `~/.fabro/workflows/`). | -| `--run-dir ` | Run output directory | +| `--storage-dir ` | Storage directory (default: `~/.fabro`) | | `--dry-run` | Execute with a simulated LLM backend | | `--auto-approve` | Auto-approve all human gates | | `--model ` | Override default LLM model | diff --git a/docs/reference/run-directory.mdx b/docs/reference/run-directory.mdx index d54da5686..e0b95c0f4 100644 --- a/docs/reference/run-directory.mdx +++ b/docs/reference/run-directory.mdx @@ -13,7 +13,7 @@ Each `fabro run` invocation creates a timestamped directory under `~/.fabro/runs ~/.fabro/runs/20260307-01JQXYZ123ABC456DEF789/ ``` -The naming format is `YYYYMMDD-{run_id}`, where `run_id` is the ULID assigned to the run. You can override the location with `--run-dir`. +The naming format is `YYYYMMDD-{run_id}`, where `run_id` is the ULID assigned to the run. You can override the base storage directory with `--storage-dir` (the runs directory will be `/runs/`). ## Root-level files diff --git a/lib/crates/fabro-api/src/demo/mod.rs b/lib/crates/fabro-api/src/demo/mod.rs index f7d418fa5..7e95d1914 100644 --- a/lib/crates/fabro-api/src/demo/mod.rs +++ b/lib/crates/fabro-api/src/demo/mod.rs @@ -3246,7 +3246,7 @@ mod settings { pub fn server_config() -> serde_json::Value { serde_json::to_value(FabroConfig { - data_dir: Some("/home/fabro/.fabro".into()), + storage_dir: Some("/home/fabro/.fabro".into()), max_concurrent_runs: Some(10), web: Some(WebConfig { url: "https://arc.example.com".into(), diff --git a/lib/crates/fabro-api/src/serve.rs b/lib/crates/fabro-api/src/serve.rs index a6962dfba..a8ff5e44f 100644 --- a/lib/crates/fabro-api/src/serve.rs +++ b/lib/crates/fabro-api/src/serve.rs @@ -84,7 +84,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: // Initialize data directory and SQLite database let config_path = args.config; let server_config = fabro_config::server::load_server_config(config_path.as_deref())?; - let data_dir = fabro_config::server::resolve_data_dir(&server_config); + let data_dir = fabro_config::server::resolve_storage_dir(&server_config); // Shared config for live reloading let shared_config = Arc::new(RwLock::new(server_config)); diff --git a/lib/crates/fabro-api/tests/openapi_conformance.rs b/lib/crates/fabro-api/tests/openapi_conformance.rs index c283749cb..2dcaae9fb 100644 --- a/lib/crates/fabro-api/tests/openapi_conformance.rs +++ b/lib/crates/fabro-api/tests/openapi_conformance.rs @@ -239,7 +239,7 @@ fn compare_schema( /// in the serialized JSON. fn fully_populated_server_config() -> FabroConfig { FabroConfig { - data_dir: Some("/data".into()), + storage_dir: Some("/data".into()), max_concurrent_runs: Some(10), web: Some(WebConfig { url: "https://example.com".into(), diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index f144276b8..1cc98adff 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -91,9 +91,9 @@ pub(crate) struct RunArgs { #[arg(required = true)] pub(crate) workflow: Option, - /// Run output directory + /// Storage directory (default: ~/.fabro) #[arg(long)] - pub(crate) run_dir: Option, + pub(crate) storage_dir: Option, /// Execute with simulated LLM backend #[arg(long)] @@ -646,9 +646,12 @@ pub(crate) enum RunCommands { /// Internal: run the engine process (reads run.json from run dir) #[command(name = "__detached", hide = true)] Detached { - /// Path to the run directory + /// Base storage directory #[arg(long)] - run_dir: PathBuf, + storage_dir: PathBuf, + /// Run ID + #[arg(long)] + run_id: String, /// Resume from checkpoint instead of fresh start #[arg(long)] resume: bool, diff --git a/lib/crates/fabro-cli/src/commands/asset/cp.rs b/lib/crates/fabro-cli/src/commands/asset/cp.rs index 5b3a26641..4976b2d9e 100644 --- a/lib/crates/fabro-cli/src/commands/asset/cp.rs +++ b/lib/crates/fabro-cli/src/commands/asset/cp.rs @@ -6,7 +6,8 @@ use crate::args::AssetCpArgs; use crate::shared::split_run_path; pub fn cp_command(args: &AssetCpArgs) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let (run_id, asset_path) = parse_source(&args.source); let run = fabro_workflows::run_lookup::resolve_run(&base, run_id)?; let entries = fabro_workflows::assets::scan_assets(&run.path, args.node.as_deref())?; diff --git a/lib/crates/fabro-cli/src/commands/asset/list.rs b/lib/crates/fabro-cli/src/commands/asset/list.rs index d8a21fb90..58a6309a4 100644 --- a/lib/crates/fabro-cli/src/commands/asset/list.rs +++ b/lib/crates/fabro-cli/src/commands/asset/list.rs @@ -4,7 +4,8 @@ use crate::args::AssetListArgs; use crate::shared::format_size; pub fn list_command(args: &AssetListArgs) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run_id)?; let entries = fabro_workflows::assets::scan_assets(&run.path, args.node.as_deref())?; diff --git a/lib/crates/fabro-cli/src/commands/pr/close.rs b/lib/crates/fabro-cli/src/commands/pr/close.rs index b3f0d7172..6ca7be3c9 100644 --- a/lib/crates/fabro-cli/src/commands/pr/close.rs +++ b/lib/crates/fabro-cli/src/commands/pr/close.rs @@ -9,7 +9,8 @@ pub async fn close_command( args: PrCloseArgs, github_app: Option, ) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); close_from(&base, args, github_app).await } diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index 41e2332da..82995b900 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -10,7 +10,8 @@ pub async fn create_command( args: PrCreateArgs, github_app: Option, ) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); create_from(&base, args, github_app).await } diff --git a/lib/crates/fabro-cli/src/commands/pr/list.rs b/lib/crates/fabro-cli/src/commands/pr/list.rs index 344246008..dc34a04eb 100644 --- a/lib/crates/fabro-cli/src/commands/pr/list.rs +++ b/lib/crates/fabro-cli/src/commands/pr/list.rs @@ -9,7 +9,8 @@ pub async fn list_command( args: PrListArgs, github_app: Option, ) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); list_from(&base, args, github_app).await } diff --git a/lib/crates/fabro-cli/src/commands/pr/merge.rs b/lib/crates/fabro-cli/src/commands/pr/merge.rs index 1f8c28716..7a257f63d 100644 --- a/lib/crates/fabro-cli/src/commands/pr/merge.rs +++ b/lib/crates/fabro-cli/src/commands/pr/merge.rs @@ -9,7 +9,8 @@ pub async fn merge_command( args: PrMergeArgs, github_app: Option, ) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); merge_from(&base, args, github_app).await } diff --git a/lib/crates/fabro-cli/src/commands/pr/view.rs b/lib/crates/fabro-cli/src/commands/pr/view.rs index 08fe0fb60..04a29bcd6 100644 --- a/lib/crates/fabro-cli/src/commands/pr/view.rs +++ b/lib/crates/fabro-cli/src/commands/pr/view.rs @@ -9,7 +9,8 @@ pub async fn view_command( args: PrViewArgs, github_app: Option, ) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); view_from(&base, args, github_app).await } diff --git a/lib/crates/fabro-cli/src/commands/preflight.rs b/lib/crates/fabro-cli/src/commands/preflight.rs index 1879ecc23..4089912ae 100644 --- a/lib/crates/fabro-cli/src/commands/preflight.rs +++ b/lib/crates/fabro-cli/src/commands/preflight.rs @@ -54,6 +54,7 @@ pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> { model: args.model.as_deref(), provider: args.provider.as_deref(), sandbox_provider, + storage_dir: None, }, ); diff --git a/lib/crates/fabro-cli/src/commands/run/cp.rs b/lib/crates/fabro-cli/src/commands/run/cp.rs index 7b90bcf4e..848cf6d6d 100644 --- a/lib/crates/fabro-cli/src/commands/run/cp.rs +++ b/lib/crates/fabro-cli/src/commands/run/cp.rs @@ -21,7 +21,8 @@ enum CopyDirection { pub async fn cp_command(args: CpArgs) -> Result<()> { let direction = parse_direction(&args.src, &args.dst)?; - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); match direction { CopyDirection::Download { diff --git a/lib/crates/fabro-cli/src/commands/run/create.rs b/lib/crates/fabro-cli/src/commands/run/create.rs index ac57babfe..2f4e7751f 100644 --- a/lib/crates/fabro-cli/src/commands/run/create.rs +++ b/lib/crates/fabro-cli/src/commands/run/create.rs @@ -7,7 +7,7 @@ use crate::args::RunArgs; use super::execute::{ apply_execution_overrides, cached_graph_path, default_run_dir, load_workflow_source_input, - parse_labels, print_diagnostics_from_error, print_workflow_report_from_persisted, + make_run_dir, parse_labels, print_diagnostics_from_error, print_workflow_report_from_persisted, resolve_sandbox_provider, write_run_config_snapshot, ExecutionOverrides, }; use fabro_util::terminal::Styles; @@ -36,10 +36,10 @@ pub async fn create_run( .run_id .clone() .unwrap_or_else(|| ulid::Ulid::new().to_string()); - let run_dir = args - .run_dir - .clone() - .unwrap_or_else(|| default_run_dir(&run_id, args.dry_run)); + let run_dir = match &args.storage_dir { + Some(sd) => make_run_dir(&sd.join("runs"), &run_id, args.dry_run), + None => default_run_dir(&run_id, args.dry_run), + }; let working_directory = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let base_branch = fabro_sandbox::daytona::detect_repo_info(&working_directory) .ok() @@ -66,6 +66,7 @@ pub async fn create_run( model: args.model.as_deref(), provider: args.provider.as_deref(), sandbox_provider, + storage_dir: args.storage_dir.as_deref(), }, ); diff --git a/lib/crates/fabro-cli/src/commands/run/detached.rs b/lib/crates/fabro-cli/src/commands/run/detached.rs index 989e0ceee..9f63f9bd4 100644 --- a/lib/crates/fabro-cli/src/commands/run/detached.rs +++ b/lib/crates/fabro-cli/src/commands/run/detached.rs @@ -11,7 +11,9 @@ use serde::Serialize; use crate::cli_config; use crate::shared; -pub async fn execute(run_dir: PathBuf, resume: bool) -> Result<()> { +pub async fn execute(storage_dir: PathBuf, run_id: String, resume: bool) -> Result<()> { + let runs_base = fabro_workflows::run_lookup::runs_base(&storage_dir); + let run_dir = fabro_workflows::run_lookup::find_run_by_prefix(&runs_base, &run_id)?; let styles: &'static fabro_util::terminal::Styles = Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr())); let cli_config = cli_config::load_cli_config(None)?; diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 9e61c10f3..043328db6 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -8,7 +8,8 @@ use crate::args::DiffArgs; pub async fn run(args: DiffArgs) -> Result<()> { info!(run_id = %args.run, "Showing diff"); - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path; let patch = resolve_diff(&run_dir, &args).await?; diff --git a/lib/crates/fabro-cli/src/commands/run/execute.rs b/lib/crates/fabro-cli/src/commands/run/execute.rs index 1cca9ab33..d7098ed66 100644 --- a/lib/crates/fabro-cli/src/commands/run/execute.rs +++ b/lib/crates/fabro-cli/src/commands/run/execute.rs @@ -56,7 +56,7 @@ pub(crate) fn resolve_cli_goal( } } -pub(crate) use fabro_workflows::operations::default_run_dir; +pub(crate) use fabro_workflows::operations::{default_run_dir, make_run_dir}; pub(crate) fn workflow_slug_from_path(workflow_path: &Path) -> Option { let file_name = workflow_path.file_name()?.to_string_lossy(); @@ -395,6 +395,7 @@ pub(crate) struct ExecutionOverrides<'a> { pub model: Option<&'a str>, pub provider: Option<&'a str>, pub sandbox_provider: SandboxProvider, + pub storage_dir: Option<&'a Path>, } pub(crate) fn apply_execution_overrides(config: &mut FabroConfig, overrides: &ExecutionOverrides) { @@ -414,6 +415,10 @@ pub(crate) fn apply_execution_overrides(config: &mut FabroConfig, overrides: &Ex if overrides.preserve_sandbox { config.sandbox.get_or_insert_default().preserve = Some(true); } + + if let Some(storage_dir) = overrides.storage_dir { + config.storage_dir = Some(storage_dir.to_path_buf()); + } } pub(crate) fn parse_labels(labels: &[String]) -> HashMap { @@ -566,7 +571,7 @@ struct RecordBasedRun { /// Used by `run_engine_entrypoint` for detached runs that already have a RunRecord on disk. pub async fn run_from_record( persisted: Persisted, - run_dir: PathBuf, + _run_dir: PathBuf, run_defaults: FabroConfig, styles: &'static Styles, github_app: Option, @@ -601,7 +606,7 @@ pub async fn run_from_record( let args = RunArgs { workflow: None, - run_dir: Some(run_dir), + storage_dir: None, dry_run: record.config.dry_run_enabled(), auto_approve: record.config.auto_approve_enabled(), @@ -641,7 +646,7 @@ pub async fn run_from_record( /// Resume an existing workflow run from its persisted checkpoint. pub async fn resume_from_record( persisted: Persisted, - run_dir: PathBuf, + _run_dir: PathBuf, run_defaults: FabroConfig, styles: &'static Styles, github_app: Option, @@ -676,7 +681,7 @@ pub async fn resume_from_record( let args = RunArgs { workflow: None, - run_dir: Some(run_dir), + storage_dir: None, dry_run: record.config.dry_run_enabled(), auto_approve: record.config.auto_approve_enabled(), @@ -798,10 +803,13 @@ async fn run_command_impl( .run_id .clone() .unwrap_or_else(|| ulid::Ulid::new().to_string()); - let run_dir = args - .run_dir - .clone() - .unwrap_or_else(|| default_run_dir(&run_id, dry_run_flag)); + let run_dir = match &workflow { + WorkflowState::Persisted(p) => p.run_dir().to_path_buf(), + _ => match &args.storage_dir { + Some(sd) => make_run_dir(&sd.join("runs"), &run_id, dry_run_flag), + None => default_run_dir(&run_id, dry_run_flag), + }, + }; if resume { ensure_resume_target_is_not_already_successful(&run_dir)?; } @@ -842,6 +850,7 @@ async fn run_command_impl( model: args.model.as_deref(), provider: args.provider.as_deref(), sandbox_provider, + storage_dir: args.storage_dir.as_deref(), }, ); diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index 8b8029df3..9fcc4ea52 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -9,7 +9,8 @@ use tracing::{debug, info}; use crate::args::LogsArgs; pub fn run(args: LogsArgs, styles: &Styles) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?; info!(run_id = %run.run_id, "Showing logs"); diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index 7a0426971..ad456b799 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -30,7 +30,8 @@ pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> { Ok(()) } RunCommands::Start { run } => { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?; let child = start::start_run(&run_info.path, false)?; eprintln!("Started engine process (PID {})", child.id()); @@ -39,7 +40,8 @@ pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> { RunCommands::Attach { run } => { let styles: &'static fabro_util::terminal::Styles = Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr())); - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?; let exit_code = attach::attach_run(&run_info.path, false, styles, None).await?; if exit_code != std::process::ExitCode::SUCCESS { @@ -47,7 +49,11 @@ pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> { } Ok(()) } - RunCommands::Detached { run_dir, resume } => detached::execute(run_dir, resume).await, + RunCommands::Detached { + storage_dir, + run_id, + resume, + } => detached::execute(storage_dir, run_id, resume).await, RunCommands::Cp(args) => cp::cp_command(args).await, RunCommands::Preview(args) => preview::run(args).await, RunCommands::Ssh(args) => ssh::run(args).await, diff --git a/lib/crates/fabro-cli/src/commands/run/preview.rs b/lib/crates/fabro-cli/src/commands/run/preview.rs index 70534e617..a15f3520e 100644 --- a/lib/crates/fabro-cli/src/commands/run/preview.rs +++ b/lib/crates/fabro-cli/src/commands/run/preview.rs @@ -5,7 +5,8 @@ use crate::args::PreviewArgs; use crate::shared::validate_daytona_provider; pub async fn run(args: PreviewArgs) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path; let sandbox_json = run_dir.join("sandbox.json"); let record = fabro_sandbox::SandboxRecord::load(&sandbox_json).context( diff --git a/lib/crates/fabro-cli/src/commands/run/resume.rs b/lib/crates/fabro-cli/src/commands/run/resume.rs index 29e275bfe..95d48c3c9 100644 --- a/lib/crates/fabro-cli/src/commands/run/resume.rs +++ b/lib/crates/fabro-cli/src/commands/run/resume.rs @@ -11,7 +11,8 @@ use crate::args::ResumeArgs; /// artifacts from the previous execution, then spawns an engine subprocess /// (identical to `fabro run`'s create→start→attach flow). pub async fn resume_command(args: ResumeArgs, styles: &'static Styles) -> anyhow::Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run_dir = fabro_workflows::run_lookup::find_run_by_prefix(&base, &args.run)?; // find_run_by_prefix can match orphan directories (no run.json). diff --git a/lib/crates/fabro-cli/src/commands/run/ssh.rs b/lib/crates/fabro-cli/src/commands/run/ssh.rs index 6b3eb262e..1da097475 100644 --- a/lib/crates/fabro-cli/src/commands/run/ssh.rs +++ b/lib/crates/fabro-cli/src/commands/run/ssh.rs @@ -5,7 +5,8 @@ use crate::args::SshArgs; use crate::shared::validate_daytona_provider; pub async fn run(args: SshArgs) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run_dir = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?.path; let sandbox_json = run_dir.join("sandbox.json"); let record = fabro_sandbox::SandboxRecord::load(&sandbox_json).context( diff --git a/lib/crates/fabro-cli/src/commands/run/start.rs b/lib/crates/fabro-cli/src/commands/run/start.rs index baf84e67b..81d82250a 100644 --- a/lib/crates/fabro-cli/src/commands/run/start.rs +++ b/lib/crates/fabro-cli/src/commands/run/start.rs @@ -9,6 +9,9 @@ use super::detached::persist_detached_failure; /// /// The engine process reads `run.json` from the run directory and executes the /// workflow. Returns the child process handle (use `.id()` for the PID). +/// +/// `storage_dir` is the base storage directory (e.g. `~/.fabro`). If `None`, +/// it is derived from the `run_dir` by stripping the `runs/` suffix. pub fn start_run(run_dir: &Path, resume: bool) -> Result { // Validate status is Submitted let status_path = run_dir.join("status.json"); @@ -56,7 +59,18 @@ pub fn start_run(run_dir: &Path, resume: bool) -> Result { return Err(err); } }; - cmd.args(["__detached", "--run-dir"]).arg(run_dir); + // Derive storage_dir (grandparent of run_dir, e.g. ~/.fabro) and run_id + let storage_dir = run_dir + .parent() // runs/ + .and_then(|p| p.parent()) // ~/.fabro/ + .unwrap_or(run_dir); + let run_id = fabro_workflows::records::RunRecord::load(run_dir) + .map(|r| r.run_id) + .or_else(|_| std::fs::read_to_string(run_dir.join("id.txt")).map(|s| s.trim().to_string())) + .unwrap_or_default(); + cmd.args(["__detached", "--storage-dir"]) + .arg(storage_dir) + .args(["--run-id", &run_id]); if resume { cmd.arg("--resume"); } diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index 62bf4d661..ac0bcf22a 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -9,7 +9,8 @@ use crate::args::WaitArgs; use crate::shared::format_duration_ms; pub fn run(args: WaitArgs, styles: &Styles) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run_info = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?; info!(run_id = %run_info.run_id, "Waiting for run to complete"); diff --git a/lib/crates/fabro-cli/src/commands/runs/inspect.rs b/lib/crates/fabro-cli/src/commands/runs/inspect.rs index c686e7a13..72b4a04c9 100644 --- a/lib/crates/fabro-cli/src/commands/runs/inspect.rs +++ b/lib/crates/fabro-cli/src/commands/runs/inspect.rs @@ -18,7 +18,8 @@ pub struct InspectOutput { } pub fn run(args: &InspectArgs) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let run = fabro_workflows::run_lookup::resolve_run(&base, &args.run)?; let output = inspect_run_dir(&run.run_id, &run.path, run.status)?; let json = serde_json::to_string_pretty(&[output])?; diff --git a/lib/crates/fabro-cli/src/commands/runs/list.rs b/lib/crates/fabro-cli/src/commands/runs/list.rs index 72da71874..11214bfa4 100644 --- a/lib/crates/fabro-cli/src/commands/runs/list.rs +++ b/lib/crates/fabro-cli/src/commands/runs/list.rs @@ -12,7 +12,8 @@ use crate::shared::{color_if, format_duration_ms, tilde_path}; use super::short_run_id; pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); let runs = fabro_workflows::run_lookup::scan_runs(&base)?; let label_filters = parse_label_filters(&args.filter.label); let filtered = fabro_workflows::run_lookup::filter_runs( diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index ffe41b438..74da6195d 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -8,7 +8,8 @@ use crate::args::RunsRemoveArgs; use super::short_run_id; pub async fn remove_command(args: &RunsRemoveArgs) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); remove_from(args, &base).await } diff --git a/lib/crates/fabro-cli/src/commands/system/df.rs b/lib/crates/fabro-cli/src/commands/system/df.rs index 29940d007..1f15f1589 100644 --- a/lib/crates/fabro-cli/src/commands/system/df.rs +++ b/lib/crates/fabro-cli/src/commands/system/df.rs @@ -9,9 +9,10 @@ use crate::args::DfArgs; use crate::shared::format_size; pub fn df_command(args: &DfArgs) -> Result<()> { - let data_dir = fabro_workflows::run_lookup::default_data_dir(); - let runs_base = fabro_workflows::run_lookup::default_runs_base(); - let logs_base = fabro_workflows::run_lookup::default_logs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let data_dir = cli_config.storage_dir(); + let runs_base = fabro_workflows::run_lookup::runs_base(&data_dir); + let logs_base = fabro_workflows::run_lookup::logs_base(&data_dir); df_from(args, &data_dir, &runs_base, &logs_base) } diff --git a/lib/crates/fabro-cli/src/commands/system/prune.rs b/lib/crates/fabro-cli/src/commands/system/prune.rs index ef5882189..fcaa06597 100644 --- a/lib/crates/fabro-cli/src/commands/system/prune.rs +++ b/lib/crates/fabro-cli/src/commands/system/prune.rs @@ -8,7 +8,8 @@ use crate::args::RunsPruneArgs; use crate::shared::format_size; pub fn prune_command(args: &RunsPruneArgs) -> Result<()> { - let base = fabro_workflows::run_lookup::default_runs_base(); + let cli_config = crate::cli_config::load_cli_config(None)?; + let base = fabro_workflows::run_lookup::runs_base(&cli_config.storage_dir()); prune_from(args, &base) } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 80f532e72..bde97e1f0 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -333,11 +333,23 @@ mod tests { #[test] fn parse_detached_command() { - let cli = Cli::try_parse_from(["fabro", "__detached", "--run-dir", "/tmp/runs/test"]) - .expect("should parse"); + let cli = Cli::try_parse_from([ + "fabro", + "__detached", + "--storage-dir", + "/tmp/fabro", + "--run-id", + "01ABC", + ]) + .expect("should parse"); match *cli.command { - Commands::RunCmd(RunCommands::Detached { run_dir, resume }) => { - assert_eq!(run_dir, std::path::PathBuf::from("/tmp/runs/test")); + Commands::RunCmd(RunCommands::Detached { + storage_dir, + run_id, + resume, + }) => { + assert_eq!(storage_dir, std::path::PathBuf::from("/tmp/fabro")); + assert_eq!(run_id, "01ABC"); assert!(!resume); } _ => panic!("unexpected command variant"), @@ -349,14 +361,21 @@ mod tests { let cli = Cli::try_parse_from([ "fabro", "__detached", - "--run-dir", - "/tmp/runs/test", + "--storage-dir", + "/tmp/fabro", + "--run-id", + "01ABC", "--resume", ]) .expect("should parse"); match *cli.command { - Commands::RunCmd(RunCommands::Detached { run_dir, resume }) => { - assert_eq!(run_dir, std::path::PathBuf::from("/tmp/runs/test")); + Commands::RunCmd(RunCommands::Detached { + storage_dir, + run_id, + resume, + }) => { + assert_eq!(storage_dir, std::path::PathBuf::from("/tmp/fabro")); + assert_eq!(run_id, "01ABC"); assert!(resume); } _ => panic!("unexpected command variant"), diff --git a/lib/crates/fabro-cli/tests/cli.rs b/lib/crates/fabro-cli/tests/cli.rs index a319e64ee..9f6320eea 100644 --- a/lib/crates/fabro-cli/tests/cli.rs +++ b/lib/crates/fabro-cli/tests/cli.rs @@ -495,20 +495,30 @@ fn doctor_no_color_when_no_color_set() { #[test] fn dry_run_writes_jsonl_and_live_json() { let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("run"); + let storage_dir = tmp.path().join("fabro-data"); arc() .args([ "run", "--dry-run", "--auto-approve", - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), "../../../test/simple.fabro", ]) .assert() .success(); + // Find the single run directory under storage_dir/runs/ + let runs_base = storage_dir.join("runs"); + assert!(runs_base.exists(), "runs/ directory should exist"); + let entries: Vec<_> = std::fs::read_dir(&runs_base) + .unwrap() + .filter_map(|e| e.ok()) + .collect(); + assert_eq!(entries.len(), 1, "should have exactly one run directory"); + let run_dir = entries[0].path(); + // progress.jsonl must exist and contain valid JSON lines let jsonl_path = run_dir.join("progress.jsonl"); assert!(jsonl_path.exists(), "progress.jsonl should exist"); @@ -555,7 +565,7 @@ fn dry_run_writes_jsonl_and_live_json() { #[test] fn run_id_passthrough_uses_provided_ulid() { let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("run"); + let storage_dir = tmp.path().join("fabro-data"); let my_ulid = "01JTEST1234567890ABCDE"; arc() @@ -565,8 +575,8 @@ fn run_id_passthrough_uses_provided_ulid() { "--auto-approve", "--run-id", my_ulid, - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), "../../../test/simple.fabro", ]) .assert() @@ -614,7 +624,7 @@ fn detach_prints_ulid_and_exits() { #[test] fn detach_creates_run_dir_with_detach_log() { let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("detached-run"); + let storage_dir = tmp.path().join("fabro-data"); let output = arc() .args([ @@ -622,8 +632,8 @@ fn detach_creates_run_dir_with_detach_log() { "--detach", "--dry-run", "--auto-approve", - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), "../../../test/simple.fabro", ]) .assert() @@ -636,8 +646,15 @@ fn detach_creates_run_dir_with_detach_log() { let ulid = ulid.trim(); assert!(!ulid.is_empty(), "should print a ULID"); - // Run dir should have been created with detach.log - assert!(run_dir.exists(), "run dir should exist"); + // Run dir should have been created under storage_dir/runs/ with detach.log + let runs_base = storage_dir.join("runs"); + assert!(runs_base.exists(), "runs/ directory should exist"); + let entries: Vec<_> = std::fs::read_dir(&runs_base) + .unwrap() + .filter_map(|e| e.ok()) + .collect(); + assert_eq!(entries.len(), 1, "should have exactly one run directory"); + let run_dir = entries[0].path(); assert!( run_dir.join("detach.log").exists(), "detach.log should exist in run dir" @@ -1015,7 +1032,8 @@ fn start_by_workflow_name_prefers_newly_created_submitted_run() { #[test] fn bug2_detached_uses_cached_graph_not_original_path() { let dir = tempfile::tempdir().unwrap(); - let run_dir = dir.path().join("run"); + let storage_dir = dir.path().join("storage"); + let run_dir = storage_dir.join("runs").join("20260101-test-bug2"); std::fs::create_dir_all(&run_dir).unwrap(); let dot = "\ @@ -1059,7 +1077,13 @@ digraph G { // __detached should use graph.fabro and never reference the deleted file. let output = arc() - .args(["__detached", "--run-dir", run_dir.to_str().unwrap()]) + .args([ + "__detached", + "--storage-dir", + storage_dir.to_str().unwrap(), + "--run-id", + "test-bug2", + ]) .env("NO_COLOR", "1") .timeout(std::time::Duration::from_secs(15)) .output() @@ -1122,7 +1146,7 @@ digraph Test { .success(); let before: serde_json::Value = serde_json::from_slice(&inspect_before.get_output().stdout).unwrap(); - let run_dir = before[0]["run_dir"].as_str().unwrap().to_string(); + let _run_dir = before[0]["run_dir"].as_str().unwrap().to_string(); let start_time_before = before[0]["start_record"]["start_time"] .as_str() .unwrap() @@ -1132,9 +1156,17 @@ digraph Test { .unwrap() .to_string(); + let storage_dir = home.path().join(".fabro"); arc() .env("HOME", home.path()) - .args(["__detached", "--run-dir", &run_dir, "--resume"]) + .args([ + "__detached", + "--storage-dir", + storage_dir.to_str().unwrap(), + "--run-id", + &run_id, + "--resume", + ]) .timeout(std::time::Duration::from_secs(10)) .assert() .failure() diff --git a/lib/crates/fabro-cli/tests/scenario.rs b/lib/crates/fabro-cli/tests/scenario.rs index e0e7fded0..6e53804d6 100644 --- a/lib/crates/fabro-cli/tests/scenario.rs +++ b/lib/crates/fabro-cli/tests/scenario.rs @@ -43,6 +43,23 @@ fn read_conclusion(run_dir: &Path) -> Value { read_json(&run_dir.join("conclusion.json")) } +/// Find the single run directory under `storage_dir/runs/`. +fn find_run_dir(storage_dir: &Path) -> PathBuf { + let runs_base = storage_dir.join("runs"); + let entries: Vec<_> = std::fs::read_dir(&runs_base) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", runs_base.display())) + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_dir()) + .collect(); + assert_eq!( + entries.len(), + 1, + "expected exactly one run directory under {}", + runs_base.display() + ); + entries[0].path() +} + fn completed_nodes(run_dir: &Path) -> Vec { let cp = read_checkpoint(run_dir); cp["completed_nodes"] @@ -105,7 +122,7 @@ scenario_tests!(command_pipeline); fn scenario_command_pipeline(sandbox: &str) { dotenvy::dotenv().ok(); let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("run"); + let storage_dir = tmp.path().join("storage"); // Validate the workflow before running it fabro() @@ -123,14 +140,15 @@ fn scenario_command_pipeline(sandbox: &str) { "--no-retro", "--sandbox", sandbox, - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), fixture("command_pipeline.fabro").to_str().unwrap(), ]) .timeout(timeout_for(sandbox)) .assert() .success(); + let run_dir = find_run_dir(&storage_dir); let conclusion = read_conclusion(&run_dir); assert_eq!( conclusion["status"].as_str(), @@ -163,7 +181,7 @@ scenario_tests!(conditional_branching); fn scenario_conditional_branching(sandbox: &str) { dotenvy::dotenv().ok(); let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("run"); + let storage_dir = tmp.path().join("storage"); fabro() .args([ @@ -172,14 +190,15 @@ fn scenario_conditional_branching(sandbox: &str) { "--no-retro", "--sandbox", sandbox, - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), fixture("conditional_branching.fabro").to_str().unwrap(), ]) .timeout(timeout_for(sandbox)) .assert() .success(); + let run_dir = find_run_dir(&storage_dir); let conclusion = read_conclusion(&run_dir); assert_eq!(conclusion["status"].as_str(), Some("success")); @@ -200,7 +219,7 @@ scenario_tests!(agent_linear); fn scenario_agent_linear(sandbox: &str) { dotenvy::dotenv().ok(); let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("run"); + let storage_dir = tmp.path().join("storage"); fabro() .args([ @@ -211,14 +230,15 @@ fn scenario_agent_linear(sandbox: &str) { sandbox, "--model", "claude-haiku-4-5", - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), fixture("agent_linear.fabro").to_str().unwrap(), ]) .timeout(timeout_for(sandbox)) .assert() .success(); + let run_dir = find_run_dir(&storage_dir); let conclusion = read_conclusion(&run_dir); assert_eq!(conclusion["status"].as_str(), Some("success")); @@ -247,7 +267,7 @@ scenario_tests!(human_gate); fn scenario_human_gate(sandbox: &str) { dotenvy::dotenv().ok(); let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("run"); + let storage_dir = tmp.path().join("storage"); fabro() .args([ @@ -258,14 +278,15 @@ fn scenario_human_gate(sandbox: &str) { sandbox, "--model", "claude-haiku-4-5", - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), fixture("human_gate.fabro").to_str().unwrap(), ]) .timeout(timeout_for(sandbox)) .assert() .success(); + let run_dir = find_run_dir(&storage_dir); let conclusion = read_conclusion(&run_dir); assert_eq!(conclusion["status"].as_str(), Some("success")); @@ -286,7 +307,7 @@ scenario_tests!(command_agent_mixed); fn scenario_command_agent_mixed(sandbox: &str) { dotenvy::dotenv().ok(); let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("run"); + let storage_dir = tmp.path().join("storage"); fabro() .args([ @@ -297,14 +318,15 @@ fn scenario_command_agent_mixed(sandbox: &str) { sandbox, "--model", "claude-haiku-4-5", - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), fixture("command_agent_mixed.fabro").to_str().unwrap(), ]) .timeout(timeout_for(sandbox)) .assert() .success(); + let run_dir = find_run_dir(&storage_dir); let conclusion = read_conclusion(&run_dir); assert_eq!(conclusion["status"].as_str(), Some("success")); @@ -337,7 +359,7 @@ scenario_tests!(full_stack); fn scenario_full_stack(sandbox: &str) { dotenvy::dotenv().ok(); let tmp = tempfile::tempdir().unwrap(); - let run_dir = tmp.path().join("run"); + let storage_dir = tmp.path().join("storage"); fabro() .args([ @@ -348,14 +370,15 @@ fn scenario_full_stack(sandbox: &str) { sandbox, "--model", "claude-haiku-4-5", - "--run-dir", - run_dir.to_str().unwrap(), + "--storage-dir", + storage_dir.to_str().unwrap(), fixture("full_stack.fabro").to_str().unwrap(), ]) .timeout(timeout_for(sandbox)) .assert() .success(); + let run_dir = find_run_dir(&storage_dir); let conclusion = read_conclusion(&run_dir); assert_eq!( conclusion["status"].as_str(), diff --git a/lib/crates/fabro-config/src/config.rs b/lib/crates/fabro-config/src/config.rs index 3c376bc26..23078dfd6 100644 --- a/lib/crates/fabro-config/src/config.rs +++ b/lib/crates/fabro-config/src/config.rs @@ -103,8 +103,8 @@ pub struct FabroConfig { pub no_retro: Option, // --- Server config fields --- - #[serde(default, skip_serializing_if = "Option::is_none")] - pub data_dir: Option, + #[serde(default, alias = "data_dir", skip_serializing_if = "Option::is_none")] + pub storage_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_concurrent_runs: Option, @@ -133,6 +133,15 @@ pub struct FabroConfig { impl FabroConfig { // --- Convenience methods (ported from CliConfig) --- + /// Resolve the storage directory: config value > default `~/.fabro`. + pub fn storage_dir(&self) -> PathBuf { + self.storage_dir.clone().unwrap_or_else(|| { + dirs::home_dir() + .expect("could not determine home directory") + .join(".fabro") + }) + } + pub fn app_id(&self) -> Option<&str> { self.git.as_ref().and_then(|g| g.app_id.as_deref()) } @@ -356,8 +365,8 @@ impl FabroConfig { } // --- Server config fields --- - if overlay.data_dir.is_some() { - self.data_dir = overlay.data_dir; + if overlay.storage_dir.is_some() { + self.storage_dir = overlay.storage_dir; } if overlay.max_concurrent_runs.is_some() { self.max_concurrent_runs = overlay.max_concurrent_runs; diff --git a/lib/crates/fabro-config/src/server.rs b/lib/crates/fabro-config/src/server.rs index 310f986f8..0efc598af 100644 --- a/lib/crates/fabro-config/src/server.rs +++ b/lib/crates/fabro-config/src/server.rs @@ -134,14 +134,9 @@ pub fn load_server_config(path: Option<&Path>) -> anyhow::Result { crate::load_config_file(path, "server.toml") } -/// Resolve the data directory: config value > default `~/.fabro`. -pub fn resolve_data_dir(config: &FabroConfig) -> PathBuf { - if let Some(ref dir) = config.data_dir { - return dir.clone(); - } - dirs::home_dir() - .map(|h| h.join(".fabro")) - .unwrap_or_else(|| PathBuf::from(".fabro")) +/// Resolve the storage directory: config value > default `~/.fabro`. +pub fn resolve_storage_dir(config: &FabroConfig) -> PathBuf { + config.storage_dir() } #[cfg(test)] @@ -149,32 +144,39 @@ mod tests { use super::*; #[test] - fn parse_config_with_data_dir() { + fn parse_config_with_storage_dir() { + let toml = r#"storage_dir = "/custom/path""#; + let config: FabroConfig = toml::from_str(toml).unwrap(); + assert_eq!(config.storage_dir, Some(PathBuf::from("/custom/path"))); + } + + #[test] + fn parse_config_with_data_dir_alias() { let toml = r#"data_dir = "/custom/path""#; let config: FabroConfig = toml::from_str(toml).unwrap(); - assert_eq!(config.data_dir, Some(PathBuf::from("/custom/path"))); + assert_eq!(config.storage_dir, Some(PathBuf::from("/custom/path"))); } #[test] fn parse_empty_config_defaults() { let toml = ""; let config: FabroConfig = toml::from_str(toml).unwrap(); - assert_eq!(config.data_dir, None); + assert_eq!(config.storage_dir, None); } #[test] - fn resolve_data_dir_uses_config_value() { + fn resolve_storage_dir_uses_config_value() { let config = FabroConfig { - data_dir: Some(PathBuf::from("/my/data")), + storage_dir: Some(PathBuf::from("/my/data")), ..FabroConfig::default() }; - assert_eq!(resolve_data_dir(&config), PathBuf::from("/my/data")); + assert_eq!(resolve_storage_dir(&config), PathBuf::from("/my/data")); } #[test] - fn resolve_data_dir_defaults_to_home_arc() { + fn resolve_storage_dir_defaults_to_home() { let config = FabroConfig::default(); - let dir = resolve_data_dir(&config); + let dir = resolve_storage_dir(&config); // Should end with .fabro assert!( dir.ends_with(".fabro"), diff --git a/lib/crates/fabro-workflows/src/operations/create.rs b/lib/crates/fabro-workflows/src/operations/create.rs index e228681b2..b65430a5f 100644 --- a/lib/crates/fabro-workflows/src/operations/create.rs +++ b/lib/crates/fabro-workflows/src/operations/create.rs @@ -221,15 +221,18 @@ pub(crate) fn finalize_config(config: &mut FabroConfig, graph: &Graph) { } pub fn default_run_dir(run_id: &str, dry_run: bool) -> PathBuf { - let base = crate::run_lookup::default_runs_base(); + make_run_dir(&crate::run_lookup::default_runs_base(), run_id, dry_run) +} + +pub fn make_run_dir(runs_base: &Path, run_id: &str, dry_run: bool) -> PathBuf { if dry_run { - base.join(format!( + runs_base.join(format!( "{}-dry-run-{}", Local::now().format("%Y%m%d"), run_id )) } else { - base.join(format!("{}-{}", Local::now().format("%Y%m%d"), run_id)) + runs_base.join(format!("{}-{}", Local::now().format("%Y%m%d"), run_id)) } } diff --git a/lib/crates/fabro-workflows/src/operations/mod.rs b/lib/crates/fabro-workflows/src/operations/mod.rs index ab1ff4bc5..83ea14909 100644 --- a/lib/crates/fabro-workflows/src/operations/mod.rs +++ b/lib/crates/fabro-workflows/src/operations/mod.rs @@ -5,8 +5,8 @@ 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, + create, create_from_file, default_run_dir, make_run_dir, validate, validate_from_file, + RunCreateOptions, ValidateOptions, }; pub use fork::fork; pub use rewind::{ diff --git a/lib/crates/fabro-workflows/src/run_lookup.rs b/lib/crates/fabro-workflows/src/run_lookup.rs index 394f8deeb..aca3a143c 100644 --- a/lib/crates/fabro-workflows/src/run_lookup.rs +++ b/lib/crates/fabro-workflows/src/run_lookup.rs @@ -38,18 +38,26 @@ pub struct RunInfo { pub is_orphan: bool, } -pub fn default_data_dir() -> PathBuf { +pub fn default_storage_dir() -> PathBuf { dirs::home_dir() .expect("could not determine home directory") .join(".fabro") } +pub fn logs_base(storage_dir: &Path) -> PathBuf { + storage_dir.join("logs") +} + pub fn default_logs_base() -> PathBuf { - default_data_dir().join("logs") + logs_base(&default_storage_dir()) +} + +pub fn runs_base(storage_dir: &Path) -> PathBuf { + storage_dir.join("runs") } pub fn default_runs_base() -> PathBuf { - default_data_dir().join("runs") + runs_base(&default_storage_dir()) } pub fn scan_runs(base: &Path) -> Result> { diff --git a/test/docs/run_tests.sh b/test/docs/run_tests.sh index 7e772b7fb..f5a47b92f 100755 --- a/test/docs/run_tests.sh +++ b/test/docs/run_tests.sh @@ -78,7 +78,7 @@ run_one() { local flags=(--auto-approve) [[ "$PHASE" == "dry-run" ]] && flags+=(--dry-run) [[ "$PHASE" == "haiku" ]] && flags+=(--model claude-haiku-4-5) - [[ "$PHASE" != "dry-run" ]] && flags+=(--run-dir "$RUNS_DIR/$(echo "$rel" | tr '/' '_')") + [[ "$PHASE" != "dry-run" ]] && flags+=(--storage-dir "$RUNS_DIR") if (cd "$dot_dir" && capture "$result_file.log" "$ARC" run start "$target" "${flags[@]}"); then echo "PASS" > "$result_file"