diff --git a/docs/agents/mcp.mdx b/docs/agents/mcp.mdx index 188438c47..5cf78dd77 100644 --- a/docs/agents/mcp.mdx +++ b/docs/agents/mcp.mdx @@ -162,7 +162,7 @@ provider = "daytona" [sandbox.daytona.snapshot] name = "daytona-medium" -[assets] +[artifacts] include = ["screenshots/**"] [mcp_servers.playwright] @@ -181,7 +181,7 @@ After startup, the agent sees 22 Playwright tools including: - `mcp__playwright__browser_type` - `mcp__playwright__browser_fill_form` -The agent uses `browser_snapshot` (accessibility tree) for structured page understanding and `browser_take_screenshot` to save visual captures. Screenshots saved to `screenshots/` are automatically collected as [assets](/execution/run-configuration#assets). +The agent uses `browser_snapshot` (accessibility tree) for structured page understanding and `browser_take_screenshot` to save visual captures. Screenshots saved to `screenshots/` are automatically collected as [artifacts](/execution/run-configuration#assets). When using Playwright MCP with the sandbox transport, call the `browser_install` tool first to ensure the Playwright browser binaries are available inside the sandbox. diff --git a/docs/agents/outputs.mdx b/docs/agents/outputs.mdx index b7d397afc..6fed29718 100644 --- a/docs/agents/outputs.mdx +++ b/docs/agents/outputs.mdx @@ -225,9 +225,9 @@ After each node executes a command, Fabro automatically scans the sandbox for te ### How asset capture works -1. **Before** the command runs, Fabro takes a baseline snapshot of known asset paths in the sandbox +1. **Before** the command runs, Fabro takes a baseline snapshot of known artifact paths in the sandbox 2. **After** the command completes, Fabro re-scans and diffs against the baseline -3. Files that are new or modified since the command started are downloaded to the stage's asset directory +3. Files that are new or modified since the command started are downloaded to the stage's artifact directory Only files modified after the command started are collected. Files that match the baseline fingerprint (same size and mtime) are skipped. Individual files over 10 MB and total collections over 50 MB are also skipped. diff --git a/docs/api-reference/fabro-api.yaml b/docs/api-reference/fabro-api.yaml index e93afb856..f64870949 100644 --- a/docs/api-reference/fabro-api.yaml +++ b/docs/api-reference/fabro-api.yaml @@ -4268,8 +4268,8 @@ components: type: array items: $ref: "#/components/schemas/HookDefinition" - assets: - $ref: "#/components/schemas/AssetsSettings" + artifacts: + $ref: "#/components/schemas/ArtifactsSettings" mcp_servers: type: object additionalProperties: @@ -4320,15 +4320,15 @@ components: type: integer description: Tool call timeout in seconds. - AssetsSettings: - description: Asset collection configuration. + ArtifactsSettings: + description: Artifact collection configuration. type: object properties: include: type: array items: type: string - description: Glob patterns for files to collect as run assets. + description: Glob patterns for files to collect as run artifacts. LogSettings: description: Logging configuration. diff --git a/docs/changelog/2026-03-08.mdx b/docs/changelog/2026-03-08.mdx index 66a60e023..292968344 100644 --- a/docs/changelog/2026-03-08.mdx +++ b/docs/changelog/2026-03-08.mdx @@ -54,7 +54,7 @@ To migrate, update any scripts or aliases: - Script nodes now execute inside the configured sandbox instead of on the host — fixes issues where agents edited files in the sandbox but lint/test commands ran on the host -- Asset collection is now opt-in via `[assets]` config with custom include globs, eliminating ~30s file scans per stage when not needed +- Artifact collection is now opt-in via `[artifacts]` config with custom include globs, eliminating ~30s file scans per stage when not needed - Workflow runs now fail immediately on git checkpoint commit failure instead of silently continuing - GitHub webhook listener via Tailscale funnel — auto-configures webhook URL on startup - `[sandbox.env]` support passes environment variables through to sandbox tool execution diff --git a/docs/changelog/2026-03-15.mdx b/docs/changelog/2026-03-15.mdx index 3453b9578..abfb9589d 100644 --- a/docs/changelog/2026-03-15.mdx +++ b/docs/changelog/2026-03-15.mdx @@ -42,7 +42,7 @@ fabro ps -a # all runs including completed ## More -- Added `fabro asset list` to view run artifacts and `fabro asset cp` to copy them locally with optional `--tree` directory structure +- Added `fabro artifact list` to view run artifacts and `fabro artifact cp` to copy them locally with optional `--tree` directory structure - Added `fabro rm` command to remove runs by ID with sandbox cleanup - Added `--direction` (`-d`) flag to `fabro graph` for overriding layout direction (`lr` or `tb`) - Added `-p` short alias for `--pretty` in `fabro logs` @@ -63,7 +63,7 @@ fabro ps -a # all runs including completed - Command output in stage preambles now truncated to the last 25-50 lines, reducing token waste from verbose build logs -- Asset collection now enforces a 100-file limit and excludes `.venv`, `venv`, `.cache`, `.tox`, `.pytest_cache`, `.mypy_cache`, and `dist` directories +- Artifact collection now enforces a 100-file limit and excludes `.venv`, `venv`, `.cache`, `.tox`, `.pytest_cache`, `.mypy_cache`, and `dist` directories - Unified dry-run behavior across all handler types — wait and human-in-the-loop nodes now properly simulate without side effects diff --git a/docs/execution/run-configuration.mdx b/docs/execution/run-configuration.mdx index ceeb2f483..00570560c 100644 --- a/docs/execution/run-configuration.mdx +++ b/docs/execution/run-configuration.mdx @@ -75,7 +75,7 @@ exclude_globs = ["**/node_modules/**", "**/.cache/**"] repo_name = "fabro" repo_url = "https://github.com/fabro-sh/fabro" -[assets] +[artifacts] include = ["test-results/**", "playwright-report/**"] [mcp_servers.playwright] @@ -258,12 +258,12 @@ digraph CI { If a `$variable` in the Graphviz file has no matching entry in `[vars]`, Fabro raises an error immediately. A bare `$` not followed by an identifier (e.g. `costs $5`) is left as-is. -### `[assets]` +### `[artifacts]` Configure automatic collection of test artifacts (Playwright reports, JUnit XML, screenshots, etc.) from the execution environment after each stage. ```toml title="run.toml" -[assets] +[artifacts] include = ["test-results/**", "playwright-report/**", "*.trace.zip"] ``` @@ -271,7 +271,7 @@ include = ["test-results/**", "playwright-report/**", "*.trace.zip"] |---|---| | `include` | Glob patterns for files to collect as assets. Matched against the working directory after each stage completes. | -Asset collection is opt-in — when no `[assets]` section is present, no file scanning occurs. This avoids the overhead of scanning large working directories when assets aren't needed. +Artifact collection is opt-in — when no `[artifacts]` section is present, no file scanning occurs. This avoids the overhead of scanning large working directories when assets aren't needed. ### `[mcp_servers]` @@ -401,7 +401,7 @@ For model and provider specifically, the precedence is: CLI flags > TOML config ### Project defaults (`fabro.toml`) -The `fabro.toml` project config can set default values for `[llm]`, `[setup]`, `[sandbox]`, `[vars]`, `[checkpoint]`, `[pull_request]`, `[github]`, `[assets]`, `[[hooks]]`, and `[mcp_servers]`. These defaults apply to all runs in the project unless the run config overrides them: +The `fabro.toml` project config can set default values for `[llm]`, `[setup]`, `[sandbox]`, `[vars]`, `[checkpoint]`, `[pull_request]`, `[github]`, `[artifacts]`, `[[hooks]]`, and `[mcp_servers]`. These defaults apply to all runs in the project unless the run config overrides them: ```toml title="fabro.toml" version = 1 diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 9aebb90b8..2cdb0a54b 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -748,14 +748,14 @@ fabro upgrade --version 0.6.0 Fabro refuses to downgrade unless you specify an explicit `--version`. A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in [`user.toml`](/reference/user-configuration#upgrade_check) or the `--no-upgrade-check` global flag. -## `fabro asset list` +## `fabro artifact list` -List assets (screenshots, test reports, traces) collected from a workflow run. +List artifacts (screenshots, test reports, traces) collected from a workflow run. ```bash -fabro asset list -fabro asset list --node verify --json -fabro asset list --node verify --retry 2 +fabro artifact list +fabro artifact list --node verify --json +fabro artifact list --node verify --retry 2 ``` | Argument / Flag | Description | @@ -765,15 +765,15 @@ fabro asset list --node verify --retry 2 | `--retry ` | Filter to assets from a specific retry attempt | | `--json` | Output as JSON | -## `fabro asset cp` +## `fabro artifact cp` -Copy assets from a workflow run to the local filesystem. +Copy artifacts from a workflow run to the local filesystem. ```bash -fabro asset cp ./output # all assets, flat -fabro asset cp ./output --tree # preserve directory structure -fabro asset cp :report.html ./output # specific file -fabro asset cp :report.html ./output --node verify --retry 2 +fabro artifact cp ./output # all assets, flat +fabro artifact cp ./output --tree # preserve directory structure +fabro artifact cp :report.html ./output # specific file +fabro artifact cp :report.html ./output --node verify --retry 2 ``` | Argument / Flag | Description | diff --git a/docs/reference/run-directory.mdx b/docs/reference/run-directory.mdx index 0d0b8f495..82d8b0394 100644 --- a/docs/reference/run-directory.mdx +++ b/docs/reference/run-directory.mdx @@ -71,10 +71,10 @@ Manager nodes that run sub-workflows write a nested `child/` directory containin **`runtime/`** — Local-only runtime files, including interview IPC files used by detached runs and `fabro attach`. -**`cache/`** — Local filesystem cache for file-backed artifacts and captured test assets: +**`cache/`** — Local filesystem cache for file-backed artifacts and captured test artifacts: - `cache/artifacts/values/` — large context values offloaded to file-backed artifacts -- `cache/artifacts/assets/` — captured test artifacts organized by node and retry +- `cache/artifacts/files/` — captured test artifacts organized by node and retry ## Browsing runs diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index 4822311b2..d74593658 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -308,33 +308,33 @@ pub(crate) struct ParseArgs { } #[derive(Args)] -pub(crate) struct AssetListArgs { +pub(crate) struct ArtifactListArgs { /// Run ID (or prefix) pub(crate) run_id: String, - /// Filter to assets from a specific node + /// Filter to artifacts from a specific node #[arg(long)] pub(crate) node: Option, - /// Filter to assets from a specific retry attempt + /// Filter to artifacts from a specific retry attempt #[arg(long)] pub(crate) retry: Option, } #[derive(Args)] -pub(crate) struct AssetCpArgs { - /// Source: RUN_ID (all assets) or RUN_ID:path (specific asset) +pub(crate) struct ArtifactCpArgs { + /// Source: RUN_ID (all artifacts) or RUN_ID:path (specific artifact) pub(crate) source: String, /// Destination directory (defaults to current directory) #[arg(default_value = ".")] pub(crate) dest: PathBuf, - /// Filter to assets from a specific node + /// Filter to artifacts from a specific node #[arg(long)] pub(crate) node: Option, - /// Filter to assets from a specific retry attempt + /// Filter to artifacts from a specific retry attempt #[arg(long)] pub(crate) retry: Option, @@ -754,8 +754,8 @@ pub(crate) enum Commands { /// Parse a DOT file and print its AST #[command(hide = true)] Parse(ParseArgs), - /// Inspect and copy run assets (screenshots, reports, traces) - Asset(AssetNamespace), + /// Inspect and copy run artifacts (screenshots, reports, traces) + Artifact(ArtifactNamespace), /// Export store-backed run state for debugging Store(StoreNamespace), #[command(flatten)] @@ -834,9 +834,9 @@ impl Commands { LlmCommand::Prompt(_) => "llm prompt", LlmCommand::Chat(_) => "llm chat", }, - Self::Asset(ns) => match &ns.command { - AssetCommand::List(_) => "asset list", - AssetCommand::Cp(_) => "asset cp", + Self::Artifact(ns) => match &ns.command { + ArtifactCommand::List(_) => "artifact list", + ArtifactCommand::Cp(_) => "artifact cp", }, Self::Store(ns) => match &ns.command { StoreCommand::Dump(_) => "store dump", @@ -925,17 +925,17 @@ pub(crate) enum PrCommand { } #[derive(Args)] -pub(crate) struct AssetNamespace { +pub(crate) struct ArtifactNamespace { #[command(subcommand)] - pub(crate) command: AssetCommand, + pub(crate) command: ArtifactCommand, } #[derive(Subcommand)] -pub(crate) enum AssetCommand { - /// List assets for a workflow run - List(AssetListArgs), - /// Copy assets from a workflow run - Cp(AssetCpArgs), +pub(crate) enum ArtifactCommand { + /// List artifacts for a workflow run + List(ArtifactListArgs), + /// Copy artifacts from a workflow run + Cp(ArtifactCpArgs), } #[derive(Args)] diff --git a/lib/crates/fabro-cli/src/commands/asset/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs similarity index 89% rename from lib/crates/fabro-cli/src/commands/asset/cp.rs rename to lib/crates/fabro-cli/src/commands/artifact/cp.rs index 980aeffc4..57168179e 100644 --- a/lib/crates/fabro-cli/src/commands/asset/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -2,29 +2,29 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; use fabro_store::RuntimeState; -use fabro_workflow::assets::{AssetEntry, scan_assets}; +use fabro_workflow::artifacts::{ArtifactEntry, scan_artifacts}; use fabro_workflow::run_lookup::{resolve_run_combined, runs_base}; -use crate::args::{AssetCpArgs, GlobalArgs}; +use crate::args::{ArtifactCpArgs, GlobalArgs}; use crate::shared::{print_json_pretty, split_run_path}; use crate::store; use crate::user_config::load_user_settings_with_globals; -pub(super) async fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Result<()> { +pub(super) async fn cp_command(args: &ArtifactCpArgs, globals: &GlobalArgs) -> Result<()> { let cli_settings = load_user_settings_with_globals(globals)?; let base = runs_base(&cli_settings.storage_dir()); let store = store::build_store(&cli_settings.storage_dir())?; let (run_id, asset_path) = parse_source(&args.source); let run = resolve_run_combined(store.as_ref(), &base, run_id).await?; let runtime_state = RuntimeState::new(&run.path); - let entries = scan_assets( - &runtime_state.assets_dir(), + let entries = scan_artifacts( + &runtime_state.artifacts_dir(), args.node.as_deref(), args.retry, )?; if entries.is_empty() { - bail!("No assets found for this run"); + bail!("No artifacts found for this run"); } std::fs::create_dir_all(&args.dest) @@ -36,7 +36,7 @@ pub(super) async fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Resu .filter(|entry| entry.relative_path == path) .collect(); if matching.is_empty() { - bail!("No asset matching path '{path}' found in this run"); + bail!("No artifact matching path '{path}' found in this run"); } if matching.len() > 1 { let candidates: Vec<_> = matching @@ -44,7 +44,7 @@ pub(super) async fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Resu .map(|entry| format!("{}:retry_{}", entry.node_slug, entry.retry)) .collect(); bail!( - "Path '{path}' matches multiple assets: {}. Use --node and/or --retry to disambiguate.", + "Path '{path}' matches multiple artifacts: {}. Use --node and/or --retry to disambiguate.", candidates.join(", ") ); } @@ -101,7 +101,7 @@ pub(super) async fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Resu })); } } else { - let mut by_filename: Vec<(String, &AssetEntry)> = Vec::with_capacity(entries.len()); + let mut by_filename: Vec<(String, &ArtifactEntry)> = Vec::with_capacity(entries.len()); for entry in &entries { let filename = Path::new(&entry.relative_path) .file_name() @@ -142,7 +142,7 @@ pub(super) async fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Resu print_json_pretty(&serde_json::json!({ "copied": copied }))?; } else { println!( - "Copied {} asset(s) to {}", + "Copied {} artifact(s) to {}", entries.len(), args.dest.display() ); @@ -157,7 +157,7 @@ fn parse_source(source: &str) -> (&str, Option<&str>) { } } -fn format_candidate(entry: &AssetEntry) -> String { +fn format_candidate(entry: &ArtifactEntry) -> String { format!("{}:retry_{}", entry.node_slug, entry.retry) } @@ -195,7 +195,7 @@ mod tests { #[test] fn format_candidate_includes_retry() { - let entry = AssetEntry { + let entry = ArtifactEntry { node_slug: "retry_assets".to_string(), retry: 2, relative_path: "assets/retry/report.txt".to_string(), diff --git a/lib/crates/fabro-cli/src/commands/asset/list.rs b/lib/crates/fabro-cli/src/commands/artifact/list.rs similarity index 82% rename from lib/crates/fabro-cli/src/commands/asset/list.rs rename to lib/crates/fabro-cli/src/commands/artifact/list.rs index 3d3b58217..afb958d80 100644 --- a/lib/crates/fabro-cli/src/commands/asset/list.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/list.rs @@ -1,21 +1,21 @@ use anyhow::Result; use fabro_store::RuntimeState; -use fabro_workflow::assets::scan_assets; +use fabro_workflow::artifacts::scan_artifacts; use fabro_workflow::run_lookup::{resolve_run_combined, runs_base}; -use crate::args::{AssetListArgs, GlobalArgs}; +use crate::args::{ArtifactListArgs, GlobalArgs}; use crate::shared::format_size; use crate::store; use crate::user_config::load_user_settings_with_globals; -pub(super) async fn list_command(args: &AssetListArgs, globals: &GlobalArgs) -> Result<()> { +pub(super) async fn list_command(args: &ArtifactListArgs, globals: &GlobalArgs) -> Result<()> { let cli_settings = load_user_settings_with_globals(globals)?; let base = runs_base(&cli_settings.storage_dir()); let store = store::build_store(&cli_settings.storage_dir())?; let run = resolve_run_combined(store.as_ref(), &base, &args.run_id).await?; let runtime_state = RuntimeState::new(&run.path); - let entries = scan_assets( - &runtime_state.assets_dir(), + let entries = scan_artifacts( + &runtime_state.artifacts_dir(), args.node.as_deref(), args.retry, )?; @@ -26,7 +26,7 @@ pub(super) async fn list_command(args: &AssetListArgs, globals: &GlobalArgs) -> } if entries.is_empty() { - println!("No assets found for this run."); + println!("No artifacts found for this run."); return Ok(()); } @@ -60,7 +60,7 @@ pub(super) async fn list_command(args: &AssetListArgs, globals: &GlobalArgs) -> } println!(); println!( - "{} asset(s), {} total", + "{} artifact(s), {} total", entries.len(), format_size(total_size) ); diff --git a/lib/crates/fabro-cli/src/commands/artifact/mod.rs b/lib/crates/fabro-cli/src/commands/artifact/mod.rs new file mode 100644 index 000000000..144fe0ea0 --- /dev/null +++ b/lib/crates/fabro-cli/src/commands/artifact/mod.rs @@ -0,0 +1,13 @@ +mod cp; +mod list; + +use anyhow::Result; + +use crate::args::{ArtifactCommand, ArtifactNamespace, GlobalArgs}; + +pub(crate) async fn dispatch(ns: ArtifactNamespace, globals: &GlobalArgs) -> Result<()> { + match ns.command { + ArtifactCommand::List(args) => list::list_command(&args, globals).await, + ArtifactCommand::Cp(args) => cp::cp_command(&args, globals).await, + } +} diff --git a/lib/crates/fabro-cli/src/commands/asset/mod.rs b/lib/crates/fabro-cli/src/commands/asset/mod.rs deleted file mode 100644 index f567f5516..000000000 --- a/lib/crates/fabro-cli/src/commands/asset/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod cp; -mod list; - -use anyhow::Result; - -use crate::args::{AssetCommand, AssetNamespace, GlobalArgs}; - -pub(crate) async fn dispatch(ns: AssetNamespace, globals: &GlobalArgs) -> Result<()> { - match ns.command { - AssetCommand::List(args) => list::list_command(&args, globals).await, - AssetCommand::Cp(args) => cp::cp_command(&args, globals).await, - } -} diff --git a/lib/crates/fabro-cli/src/commands/mod.rs b/lib/crates/fabro-cli/src/commands/mod.rs index 5a55fe548..2e4d171a6 100644 --- a/lib/crates/fabro-cli/src/commands/mod.rs +++ b/lib/crates/fabro-cli/src/commands/mod.rs @@ -1,4 +1,4 @@ -pub(crate) mod asset; +pub(crate) mod artifact; pub(crate) mod config; pub(crate) mod doctor; pub(crate) mod exec; diff --git a/lib/crates/fabro-cli/src/commands/run/output.rs b/lib/crates/fabro-cli/src/commands/run/output.rs index 0043a537b..8036d7acc 100644 --- a/lib/crates/fabro-cli/src/commands/run/output.rs +++ b/lib/crates/fabro-cli/src/commands/run/output.rs @@ -7,7 +7,7 @@ use fabro_store::RuntimeState; use fabro_types::PullRequestRecord; use fabro_util::terminal::Styles; use fabro_util::text::strip_goal_decoration; -use fabro_workflow::asset_snapshot::collect_asset_paths; +use fabro_workflow::artifact_snapshot::collect_artifact_paths; use fabro_workflow::outcome::{StageStatus, format_cost}; use fabro_workflow::pipeline::{Persisted, Validated}; use fabro_workflow::records::Conclusion; @@ -229,12 +229,12 @@ pub(crate) async fn print_final_output( pub(crate) fn print_assets(run_dir: &Path, styles: &Styles) { let runtime_state = RuntimeState::new(run_dir); - let paths = collect_asset_paths(&runtime_state.assets_dir()); + let paths = collect_artifact_paths(&runtime_state.artifacts_dir()); if paths.is_empty() { return; } let home = dirs::home_dir(); - eprintln!("\n{}", styles.bold.apply_to("=== Assets ===")); + eprintln!("\n{}", styles.bold.apply_to("=== Artifacts ===")); for path in &paths { let display = match &home { Some(home_dir) => { diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index 81c66e2d4..75e2ed746 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -536,12 +536,12 @@ mod tests { .unwrap(); let summary_blob = run.write_blob(br#"{"done":true}"#).await.unwrap(); let plan_blob = run.write_blob(br#"{"steps":3}"#).await.unwrap(); - run.put_asset(&node, "src/lib.rs", b"fn main() {}") + run.put_artifact(&node, "src/lib.rs", b"fn main() {}") .await .unwrap(); - let asset_only_node = StageId::new("artifact-only", 7); - run.put_asset(&asset_only_node, "logs/output.txt", b"hello") + let artifact_only_node = StageId::new("artifact-only", 7); + run.put_artifact(&artifact_only_node, "logs/output.txt", b"hello") .await .unwrap(); @@ -665,14 +665,14 @@ mod tests { ) .await .unwrap(); - run.put_asset(&StageId::new("code", 1), "../escape.txt", b"boom") + run.put_artifact(&StageId::new("code", 1), "../escape.txt", b"boom") .await .unwrap(); let temp = tempfile::tempdir().unwrap(); let output = temp.path().join("dump"); let err = export_run(&run, &output).await.unwrap_err(); - assert!(err.to_string().contains("asset filename")); + assert!(err.to_string().contains("artifact filename")); assert!(!output.exists()); } diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 42faca406..7b3c0afe7 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -174,7 +174,7 @@ async fn main_inner() -> (String, Result<()>) { Commands::Parse(args) => { commands::parse::run(&args, &globals)?; } - Commands::Asset(ns) => commands::asset::dispatch(ns, &globals).await?, + Commands::Artifact(ns) => commands::artifact::dispatch(ns, &globals).await?, Commands::Store(ns) => commands::store::dispatch(ns, &globals).await?, Commands::RunsCmd(cmd) => commands::runs::dispatch(cmd, &globals).await?, Commands::Model { command } => commands::model::execute(command, &globals).await?, diff --git a/lib/crates/fabro-cli/tests/it/cmd/asset.rs b/lib/crates/fabro-cli/tests/it/cmd/artifact.rs similarity index 82% rename from lib/crates/fabro-cli/tests/it/cmd/asset.rs rename to lib/crates/fabro-cli/tests/it/cmd/artifact.rs index 62b42a10d..00d0de303 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/asset.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/artifact.rs @@ -4,18 +4,18 @@ use fabro_test::{fabro_snapshot, test_context}; fn help() { let context = test_context!(); let mut cmd = context.command(); - cmd.args(["asset", "--help"]); + cmd.args(["artifact", "--help"]); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 ----- stdout ----- - Inspect and copy run assets (screenshots, reports, traces) + Inspect and copy run artifacts (screenshots, reports, traces) - Usage: fabro asset [OPTIONS] + Usage: fabro artifact [OPTIONS] Commands: - list List assets for a workflow run - cp Copy assets from a workflow run + list List artifacts for a workflow run + cp Copy artifacts from a workflow run help Print this message or the help of the given subcommand(s) Options: diff --git a/lib/crates/fabro-cli/tests/it/cmd/asset_cp.rs b/lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs similarity index 66% rename from lib/crates/fabro-cli/tests/it/cmd/asset_cp.rs rename to lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs index 1ae8d09ae..ef9395635 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/asset_cp.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs @@ -1,29 +1,29 @@ use fabro_test::{fabro_snapshot, test_context}; -use super::support::{read_text, setup_asset_run, setup_completed_dry_run, text_tree}; +use super::support::{read_text, setup_artifact_run, setup_completed_dry_run, text_tree}; #[test] fn help() { let context = test_context!(); let mut cmd = context.command(); - cmd.args(["asset", "cp", "--help"]); + cmd.args(["artifact", "cp", "--help"]); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 ----- stdout ----- - Copy assets from a workflow run + Copy artifacts from a workflow run - Usage: fabro asset cp [OPTIONS] [DEST] + Usage: fabro artifact cp [OPTIONS] [DEST] Arguments: - Source: RUN_ID (all assets) or RUN_ID:path (specific asset) + Source: RUN_ID (all artifacts) or RUN_ID:path (specific artifact) [DEST] Destination directory (defaults to current directory) [default: .] Options: --json Output as JSON [env: FABRO_JSON=] - --node Filter to assets from a specific node + --node Filter to artifacts from a specific node --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] - --retry Filter to assets from a specific retry attempt + --retry Filter to artifacts from a specific retry attempt --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] --tree Preserve {node_slug}/retry_{N}/ directory structure --quiet Suppress non-essential output [env: FABRO_QUIET=] @@ -36,30 +36,30 @@ fn help() { } #[test] -fn asset_cp_empty_run_reports_no_assets() { +fn artifact_cp_empty_run_reports_no_artifacts() { let context = test_context!(); let run = setup_completed_dry_run(&context); - let dest = context.temp_dir.join("asset-dest"); + let dest = context.temp_dir.join("artifact-dest"); let mut cmd = context.command(); - cmd.args(["asset", "cp", &run.run_id, dest.to_str().unwrap()]); + cmd.args(["artifact", "cp", &run.run_id, dest.to_str().unwrap()]); fabro_snapshot!(context.filters(), cmd, @" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - error: No assets found for this run + error: No artifacts found for this run "); } #[test] -fn asset_cp_specific_path_copies_single_asset() { +fn artifact_cp_specific_path_copies_single_asset() { let context = test_context!(); - let setup = setup_asset_run(&context); - let dest = context.temp_dir.join("asset-one"); + let setup = setup_artifact_run(&context); + let dest = context.temp_dir.join("artifact-one"); let mut cmd = context.command(); cmd.args([ - "asset", + "artifact", "cp", &format!("{}:assets/shared/report.txt", setup.run.run_id), dest.to_str().unwrap(), @@ -71,20 +71,20 @@ fn asset_cp_specific_path_copies_single_asset() { success: true exit_code: 0 ----- stdout ----- - Copied assets/shared/report.txt to [TEMP_DIR]/asset-one/report.txt + Copied assets/shared/report.txt to [TEMP_DIR]/artifact-one/report.txt ----- stderr ----- "); assert_eq!(read_text(&dest.join("report.txt")), "one"); } #[test] -fn asset_cp_ambiguous_path_requires_node_or_retry() { +fn artifact_cp_ambiguous_path_requires_node_or_retry() { let context = test_context!(); - let setup = setup_asset_run(&context); - let dest = context.temp_dir.join("asset-one"); + let setup = setup_artifact_run(&context); + let dest = context.temp_dir.join("artifact-one"); let mut cmd = context.command(); cmd.args([ - "asset", + "artifact", "cp", &format!("{}:assets/retry/report.txt", setup.run.run_id), dest.to_str().unwrap(), @@ -95,18 +95,18 @@ fn asset_cp_ambiguous_path_requires_node_or_retry() { exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Path 'assets/retry/report.txt' matches multiple assets: create_colliding:retry_1, retry_assets:retry_1, retry_assets:retry_2. Use --node and/or --retry to disambiguate. + error: Path 'assets/retry/report.txt' matches multiple artifacts: create_colliding:retry_1, retry_assets:retry_1, retry_assets:retry_2. Use --node and/or --retry to disambiguate. "); } #[test] -fn asset_cp_tree_preserves_structure() { +fn artifact_cp_tree_preserves_structure() { let context = test_context!(); - let setup = setup_asset_run(&context); - let dest = context.temp_dir.join("asset-tree"); + let setup = setup_artifact_run(&context); + let dest = context.temp_dir.join("artifact-tree"); let mut cmd = context.command(); cmd.args([ - "asset", + "artifact", "cp", &setup.run.run_id, dest.to_str().unwrap(), @@ -117,7 +117,7 @@ fn asset_cp_tree_preserves_structure() { success: true exit_code: 0 ----- stdout ----- - Copied 6 asset(s) to [TEMP_DIR]/asset-tree + Copied 6 artifact(s) to [TEMP_DIR]/artifact-tree ----- stderr ----- "); insta::assert_snapshot!( @@ -134,12 +134,12 @@ fn asset_cp_tree_preserves_structure() { } #[test] -fn asset_cp_flat_mode_rejects_filename_collisions() { +fn artifact_cp_flat_mode_rejects_filename_collisions() { let context = test_context!(); - let setup = setup_asset_run(&context); - let dest = context.temp_dir.join("asset-flat"); + let setup = setup_artifact_run(&context); + let dest = context.temp_dir.join("artifact-flat"); let mut cmd = context.command(); - cmd.args(["asset", "cp", &setup.run.run_id, dest.to_str().unwrap()]); + cmd.args(["artifact", "cp", &setup.run.run_id, dest.to_str().unwrap()]); fabro_snapshot!(context.filters(), cmd, @" success: false diff --git a/lib/crates/fabro-cli/tests/it/cmd/asset_list.rs b/lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs similarity index 68% rename from lib/crates/fabro-cli/tests/it/cmd/asset_list.rs rename to lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs index a9bedf5cd..bccf14e15 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/asset_list.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs @@ -1,28 +1,28 @@ use fabro_test::{fabro_snapshot, test_context}; -use super::support::{setup_asset_run, setup_completed_dry_run}; +use super::support::{setup_artifact_run, setup_completed_dry_run}; #[test] fn help() { let context = test_context!(); let mut cmd = context.command(); - cmd.args(["asset", "list", "--help"]); + cmd.args(["artifact", "list", "--help"]); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 ----- stdout ----- - List assets for a workflow run + List artifacts for a workflow run - Usage: fabro asset list [OPTIONS] + Usage: fabro artifact list [OPTIONS] Arguments: Run ID (or prefix) Options: --json Output as JSON [env: FABRO_JSON=] - --node Filter to assets from a specific node + --node Filter to artifacts from a specific node --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] - --retry Filter to assets from a specific retry attempt + --retry Filter to artifacts from a specific retry attempt --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] --quiet Suppress non-essential output [env: FABRO_QUIET=] --verbose Enable verbose output [env: FABRO_VERBOSE=] @@ -34,32 +34,32 @@ fn help() { } #[test] -fn asset_list_empty_run_reports_no_assets() { +fn artifact_list_empty_run_reports_no_artifacts() { let context = test_context!(); let run = setup_completed_dry_run(&context); let mut cmd = context.command(); - cmd.args(["asset", "list", &run.run_id]); + cmd.args(["artifact", "list", &run.run_id]); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 ----- stdout ----- - No assets found for this run. + No artifacts found for this run. ----- stderr ----- "); } #[test] -fn asset_list_json_outputs_entries() { +fn artifact_list_json_outputs_entries() { let context = test_context!(); - let setup = setup_asset_run(&context); + let setup = setup_artifact_run(&context); let mut filters = context.filters(); filters.push(( r"\[STORAGE_DIR\]/runs/\d{8}-\[ULID\]".to_string(), "[RUN_DIR]".to_string(), )); let mut cmd = context.command(); - cmd.args(["asset", "list", &setup.run.run_id, "--json"]); + cmd.args(["artifact", "list", &setup.run.run_id, "--json"]); fabro_snapshot!(filters, cmd, @r#" success: true @@ -70,42 +70,42 @@ fn asset_list_json_outputs_entries() { "node_slug": "create_assets", "retry": 1, "relative_path": "assets/node_a/summary.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/assets/create_assets/retry_1/assets/node_a/summary.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_assets/retry_1/assets/node_a/summary.txt", "size": 5 }, { "node_slug": "create_assets", "retry": 1, "relative_path": "assets/shared/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/assets/create_assets/retry_1/assets/shared/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_assets/retry_1/assets/shared/report.txt", "size": 3 }, { "node_slug": "create_colliding", "retry": 1, "relative_path": "assets/other/summary.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/assets/create_colliding/retry_1/assets/other/summary.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_colliding/retry_1/assets/other/summary.txt", "size": 4 }, { "node_slug": "create_colliding", "retry": 1, "relative_path": "assets/retry/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/assets/create_colliding/retry_1/assets/retry/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_colliding/retry_1/assets/retry/report.txt", "size": 6 }, { "node_slug": "retry_assets", "retry": 1, "relative_path": "assets/retry/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/assets/retry_assets/retry_1/assets/retry/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_1/assets/retry/report.txt", "size": 5 }, { "node_slug": "retry_assets", "retry": 2, "relative_path": "assets/retry/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/assets/retry_assets/retry_2/assets/retry/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_2/assets/retry/report.txt", "size": 6 } ] @@ -114,9 +114,9 @@ fn asset_list_json_outputs_entries() { } #[test] -fn asset_list_filters_by_node_and_retry() { +fn artifact_list_filters_by_node_and_retry() { let context = test_context!(); - let setup = setup_asset_run(&context); + let setup = setup_artifact_run(&context); let mut filters = context.filters(); filters.push(( r"\[STORAGE_DIR\]/runs/\d{8}-\[ULID\]".to_string(), @@ -124,7 +124,7 @@ fn asset_list_filters_by_node_and_retry() { )); let mut cmd = context.command(); cmd.args([ - "asset", + "artifact", "list", &setup.run.run_id, "--node", @@ -143,7 +143,7 @@ fn asset_list_filters_by_node_and_retry() { "node_slug": "retry_assets", "retry": 2, "relative_path": "assets/retry/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/assets/retry_assets/retry_2/assets/retry/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_2/assets/retry/report.txt", "size": 6 } ] diff --git a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs index 5aa785231..6fb94e38b 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs @@ -24,7 +24,7 @@ fn help() { preflight Validate run configuration without executing validate Validate a workflow graph Render a workflow graph as SVG or PNG - asset Inspect and copy run assets (screenshots, reports, traces) + artifact Inspect and copy run artifacts (screenshots, reports, traces) store Export store-backed run state for debugging rm Remove one or more workflow runs inspect Show detailed information about a workflow run diff --git a/lib/crates/fabro-cli/tests/it/cmd/mod.rs b/lib/crates/fabro-cli/tests/it/cmd/mod.rs index 5a517c6e6..edece028c 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/mod.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/mod.rs @@ -1,6 +1,6 @@ -mod asset; -mod asset_cp; -mod asset_list; +mod artifact; +mod artifact_cp; +mod artifact_list; mod attach; mod completion; mod config; diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index a7382ac00..15d437815 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -203,15 +203,15 @@ impl WorkflowGate { } } -pub(crate) fn setup_asset_run(context: &TestContext) -> WorkspaceRunSetup { - let workspace_dir = context.temp_dir.join("asset-run"); +pub(crate) fn setup_artifact_run(context: &TestContext) -> WorkspaceRunSetup { + let workspace_dir = context.temp_dir.join("artifact-run"); std::fs::create_dir_all(&workspace_dir) .unwrap_or_else(|err| panic!("failed to create {}: {err}", workspace_dir.display())); write_text_file( - &workspace_dir.join("asset_run.fabro"), - r#"digraph AssetRun { - graph [goal="Exercise asset commands", default_max_retries=0] + &workspace_dir.join("artifact_run.fabro"), + r#"digraph ArtifactRun { + graph [goal="Exercise artifact commands", default_max_retries=0] start [shape=Mdiamond] exit [shape=Msquare] create_assets [shape=parallelogram, script="mkdir -p assets/shared assets/node_a && printf one > assets/shared/report.txt && printf alpha > assets/node_a/summary.txt", max_retries=0] @@ -224,8 +224,8 @@ pub(crate) fn setup_asset_run(context: &TestContext) -> WorkspaceRunSetup { write_text_file( &workspace_dir.join("run.toml"), r#"version = 1 -graph = "asset_run.fabro" -goal = "Exercise asset commands" +graph = "artifact_run.fabro" +goal = "Exercise artifact commands" [sandbox] provider = "local" @@ -234,7 +234,7 @@ preserve = true [sandbox.local] worktree_mode = "never" -[assets] +[artifacts] include = ["assets/**"] "#, ); @@ -242,9 +242,9 @@ include = ["assets/**"] let run = run_local_workflow(context, &workspace_dir, "run.toml"); assert!( run.run_dir - .join("cache/artifacts/assets/retry_assets/retry_2/manifest.json") + .join("cache/artifacts/files/retry_assets/retry_2/manifest.json") .exists(), - "setup_asset_run should materialize retry_2 assets" + "setup_artifact_run should materialize retry_2 assets" ); WorkspaceRunSetup { run, workspace_dir } diff --git a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs index a928d6a47..77e35455e 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs @@ -69,12 +69,13 @@ fn local_run_lifecycle() { "first log line should have an event field" ); - // 5. asset list — no assets yet, should succeed with empty message - let asset_list_out = cmd(&["asset", "list", &run_id]).success(); - let asset_list_stdout = String::from_utf8(asset_list_out.get_output().stdout.clone()).unwrap(); + // 5. artifact list — no assets yet, should succeed with empty message + let artifact_list_out = cmd(&["artifact", "list", &run_id]).success(); + let artifact_list_stdout = + String::from_utf8(artifact_list_out.get_output().stdout.clone()).unwrap(); assert!( - asset_list_stdout.contains("No assets found"), - "asset list should report no assets: {asset_list_stdout}" + artifact_list_stdout.contains("No artifacts found"), + "artifact list should report no artifacts: {artifact_list_stdout}" ); // 6. system df — mentions "Runs" diff --git a/lib/crates/fabro-config/src/config.rs b/lib/crates/fabro-config/src/config.rs index a70b69f6a..72abe987d 100644 --- a/lib/crates/fabro-config/src/config.rs +++ b/lib/crates/fabro-config/src/config.rs @@ -8,7 +8,7 @@ use crate::hook::{HookDefinition, HookSettings}; use crate::mcp::McpServerEntry; use crate::project::{self, ProjectConfig}; use crate::run::{ - AssetsConfig, CheckpointConfig, GitHubConfig, LlmConfig, PullRequestConfig, SetupConfig, + ArtifactsConfig, CheckpointConfig, GitHubConfig, LlmConfig, PullRequestConfig, SetupConfig, }; use crate::sandbox::SandboxConfig; use crate::server::{self, ApiConfig, FeaturesConfig, GitConfig, LogConfig, WebConfig}; @@ -65,7 +65,7 @@ pub struct ConfigLayer { pub pull_request: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub assets: Option, + pub artifacts: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub hooks: Vec, @@ -157,7 +157,7 @@ impl Combine for ConfigLayer { vars: self.vars.combine(other.vars), checkpoint: self.checkpoint.combine(other.checkpoint), pull_request: self.pull_request.combine(other.pull_request), - assets: self.assets.combine(other.assets), + artifacts: self.artifacts.combine(other.artifacts), hooks, mcp_servers: self.mcp_servers.combine(other.mcp_servers), github: self.github.combine(other.github), diff --git a/lib/crates/fabro-config/src/run.rs b/lib/crates/fabro-config/src/run.rs index 20feda467..5038af905 100644 --- a/lib/crates/fabro-config/src/run.rs +++ b/lib/crates/fabro-config/src/run.rs @@ -9,7 +9,7 @@ use crate::combine::Combine; use crate::config::ConfigLayer; use crate::sandbox::DockerfileSource; pub use fabro_types::settings::run::{ - AssetsSettings, CheckpointSettings, GitHubSettings, LlmSettings, MergeStrategy, + ArtifactsSettings, CheckpointSettings, GitHubSettings, LlmSettings, MergeStrategy, PullRequestSettings, SetupSettings, }; @@ -59,13 +59,13 @@ impl From for PullRequestSettings { } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)] -pub struct AssetsConfig { +pub struct ArtifactsConfig { #[serde(default)] pub include: Vec, } -impl From for AssetsSettings { - fn from(value: AssetsConfig) -> Self { +impl From for ArtifactsSettings { + fn from(value: ArtifactsConfig) -> Self { Self { include: value.include, } diff --git a/lib/crates/fabro-config/src/settings.rs b/lib/crates/fabro-config/src/settings.rs index f0ec2c2f5..8918df2fb 100644 --- a/lib/crates/fabro-config/src/settings.rs +++ b/lib/crates/fabro-config/src/settings.rs @@ -19,7 +19,7 @@ impl TryFrom for Settings { vars: value.vars, checkpoint: value.checkpoint.into(), pull_request: value.pull_request.map(Into::into), - assets: value.assets.map(Into::into), + artifacts: value.artifacts.map(Into::into), hooks: value.hooks, mcp_servers: value.mcp_servers, github: value.github.map(Into::into), diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 06e20c6da..e5818f92a 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -1327,7 +1327,7 @@ mod runs { hooks: vec![], checkpoint: Default::default(), pull_request: None, - assets: None, + artifacts: None, mcp_servers: Default::default(), github: None, ..Default::default() @@ -1496,7 +1496,7 @@ mod workflows { hooks: vec![], checkpoint: Default::default(), pull_request: None, - assets: None, + artifacts: None, mcp_servers: Default::default(), github: None, ..Default::default() @@ -1569,7 +1569,7 @@ mod workflows { hooks: vec![], checkpoint: Default::default(), pull_request: None, - assets: None, + artifacts: None, mcp_servers: Default::default(), github: None, ..Default::default() @@ -1654,7 +1654,7 @@ mod workflows { hooks: vec![], checkpoint: Default::default(), pull_request: None, - assets: None, + artifacts: None, mcp_servers: Default::default(), github: None, ..Default::default() @@ -1728,7 +1728,7 @@ mod workflows { hooks: vec![], checkpoint: Default::default(), pull_request: None, - assets: None, + artifacts: None, mcp_servers: Default::default(), github: None, ..Default::default() @@ -3505,7 +3505,7 @@ mod settings { vars: None, checkpoint: Default::default(), pull_request: None, - assets: None, + artifacts: None, hooks: vec![], mcp_servers: Default::default(), github: None, diff --git a/lib/crates/fabro-server/tests/it/openapi_conformance.rs b/lib/crates/fabro-server/tests/it/openapi_conformance.rs index 0de4c733b..abdfd547d 100644 --- a/lib/crates/fabro-server/tests/it/openapi_conformance.rs +++ b/lib/crates/fabro-server/tests/it/openapi_conformance.rs @@ -308,7 +308,7 @@ fn fully_populated_server_config() -> Settings { auto_merge: false, merge_strategy: MergeStrategy::Squash, }), - assets: Some(AssetsSettings { + artifacts: Some(ArtifactsSettings { include: vec!["test-results/**".into()], }), // One hook per HookType variant so the key union covers all fields. diff --git a/lib/crates/fabro-store/src/keys.rs b/lib/crates/fabro-store/src/keys.rs index 2669a0bc8..c598bedea 100644 --- a/lib/crates/fabro-store/src/keys.rs +++ b/lib/crates/fabro-store/src/keys.rs @@ -18,7 +18,7 @@ pub(crate) fn blob_key(id: &RunBlobId) -> String { format!("{BLOBS_PREFIX}{id}") } -pub(crate) fn node_asset_prefix(node: &StageId) -> String { +pub(crate) fn node_artifact_prefix(node: &StageId) -> String { format!( "{ARTIFACT_NODES_PREFIX}{}#visit-{}", node.node_id(), @@ -26,8 +26,8 @@ pub(crate) fn node_asset_prefix(node: &StageId) -> String { ) } -pub(crate) fn node_asset(node: &StageId, filename: &str) -> String { - format!("{}#{filename}", node_asset_prefix(node)) +pub(crate) fn node_artifact(node: &StageId, filename: &str) -> String { + format!("{}#{filename}", node_artifact_prefix(node)) } pub(crate) fn parse_event_seq(key: &str) -> Option { @@ -38,7 +38,7 @@ pub(crate) fn parse_blob_id(key: &str) -> Option { key.strip_prefix(BLOBS_PREFIX)?.parse().ok() } -pub(crate) fn parse_node_asset_key(key: &str) -> Option<(StageId, String)> { +pub(crate) fn parse_node_artifact_key(key: &str) -> Option<(StageId, String)> { parse_visit_scoped_key(key, ARTIFACT_NODES_PREFIX) } @@ -74,7 +74,7 @@ mod tests { let blob_id = RunBlobId::new(&"01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(), b"summary"); assert_eq!(blob_key(&blob_id), format!("blobs#{blob_id}")); assert_eq!( - node_asset(&node, "src/main.rs"), + node_artifact(&node, "src/main.rs"), "artifacts#nodes#code#visit-2#src/main.rs" ); } @@ -85,7 +85,7 @@ mod tests { let blob_id = RunBlobId::new(&"01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(), b"summary"); assert_eq!(parse_blob_id(&format!("blobs#{blob_id}")), Some(blob_id)); assert_eq!( - parse_node_asset_key("artifacts#nodes#code#visit-2#src/main.rs"), + parse_node_artifact_key("artifacts#nodes#code#visit-2#src/main.rs"), Some((StageId::new("code", 2), "src/main.rs".to_string())) ); } @@ -95,7 +95,7 @@ mod tests { assert_eq!(parse_event_seq("events#not-a-seq.json"), None); assert_eq!(parse_blob_id("blobs#not-a-uuid"), None); assert_eq!( - parse_node_asset_key("artifacts#nodes#code#status.json"), + parse_node_artifact_key("artifacts#nodes#code#status.json"), None ); } @@ -103,7 +103,7 @@ mod tests { #[test] fn asset_filename_with_slashes_parses_correctly() { assert_eq!( - parse_node_asset_key("artifacts#nodes#build#visit-1#deep/nested/path/file.rs"), + parse_node_artifact_key("artifacts#nodes#build#visit-1#deep/nested/path/file.rs"), Some(( StageId::new("build", 1), "deep/nested/path/file.rs".to_string() diff --git a/lib/crates/fabro-store/src/lib.rs b/lib/crates/fabro-store/src/lib.rs index 7b47df0da..9be4541f4 100644 --- a/lib/crates/fabro-store/src/lib.rs +++ b/lib/crates/fabro-store/src/lib.rs @@ -13,7 +13,7 @@ pub use error::{Result, StoreError}; pub use fabro_types::{RunBlobId, StageId}; pub use run_state::{NodeState, RunProjection}; pub use runtime::RuntimeState; -pub use slate::{NodeAsset, SlateRunStore, SlateStore}; +pub use slate::{NodeArtifact, SlateRunStore, SlateStore}; pub use types::{EventEnvelope, EventPayload, RunSummary}; pub type StoreHandle = Arc; diff --git a/lib/crates/fabro-store/src/runtime.rs b/lib/crates/fabro-store/src/runtime.rs index 28efd7f32..5fe2e41df 100644 --- a/lib/crates/fabro-store/src/runtime.rs +++ b/lib/crates/fabro-store/src/runtime.rs @@ -34,24 +34,28 @@ impl RuntimeState { } #[must_use] - pub fn artifact_values_dir(&self) -> PathBuf { + pub fn blob_cache_dir(&self) -> PathBuf { self.root.join("cache").join("artifacts").join("values") } + #[must_use] + pub fn artifact_values_dir(&self) -> PathBuf { + self.blob_cache_dir() + } + #[must_use] pub fn artifact_value_path(&self, artifact_id: &str) -> PathBuf { - self.artifact_values_dir() - .join(format!("{artifact_id}.json")) + self.blob_cache_dir().join(format!("{artifact_id}.json")) } #[must_use] - pub fn assets_dir(&self) -> PathBuf { - self.root.join("cache").join("artifacts").join("assets") + pub fn artifacts_dir(&self) -> PathBuf { + self.root.join("cache").join("artifacts").join("files") } #[must_use] - pub fn asset_stage_dir(&self, node_slug: &str, attempt: u32) -> PathBuf { - self.assets_dir() + pub fn artifact_stage_dir(&self, node_slug: &str, attempt: u32) -> PathBuf { + self.artifacts_dir() .join(node_slug) .join(format!("retry_{attempt}")) } @@ -80,7 +84,7 @@ mod tests { dir.path().join("runtime").join("interview_request.claim") ); assert_eq!( - state.artifact_values_dir(), + state.blob_cache_dir(), dir.path().join("cache").join("artifacts").join("values") ); assert_eq!( @@ -92,15 +96,15 @@ mod tests { .join("response.plan.json") ); assert_eq!( - state.assets_dir(), - dir.path().join("cache").join("artifacts").join("assets") + state.artifacts_dir(), + dir.path().join("cache").join("artifacts").join("files") ); assert_eq!( - state.asset_stage_dir("plan", 2), + state.artifact_stage_dir("plan", 2), dir.path() .join("cache") .join("artifacts") - .join("assets") + .join("files") .join("plan") .join("retry_2") ); diff --git a/lib/crates/fabro-store/src/slate/mod.rs b/lib/crates/fabro-store/src/slate/mod.rs index 85350a4ff..e3e9f885c 100644 --- a/lib/crates/fabro-store/src/slate/mod.rs +++ b/lib/crates/fabro-store/src/slate/mod.rs @@ -16,7 +16,7 @@ use crate::keys; use crate::{ListRunsQuery, Result, RunSummary, StoreError}; use fabro_types::RunId; use run_store::SlateRunStoreInner; -pub use run_store::{NodeAsset, SlateRunStore}; +pub use run_store::{NodeArtifact, SlateRunStore}; #[derive(Clone)] pub struct SlateStore { @@ -1181,7 +1181,7 @@ mod tests { )) .await .unwrap(); - run.put_asset(&node, "src/lib.rs", b"fn main() {}") + run.put_artifact(&node, "src/lib.rs", b"fn main() {}") .await .unwrap(); @@ -1189,7 +1189,7 @@ mod tests { let node_state = state.node(&node).unwrap(); assert_eq!(node_state.prompt, Some("Plan".to_string())); assert_eq!( - run.get_asset(&node, "src/lib.rs").await.unwrap(), + run.get_artifact(&node, "src/lib.rs").await.unwrap(), Some(Bytes::from_static(b"fn main() {}")) ); } @@ -1203,12 +1203,12 @@ mod tests { let plan_blob = run.write_blob(br#"{"steps":3}"#).await.unwrap(); let snapshot_node = StageId::new("code", 2); - run.put_asset(&snapshot_node, "src/lib.rs", b"fn main() {}") + run.put_artifact(&snapshot_node, "src/lib.rs", b"fn main() {}") .await .unwrap(); - let asset_only_node = StageId::new("artifact-only", 7); - run.put_asset(&asset_only_node, "logs/output.txt", b"hello") + let artifact_only_node = StageId::new("artifact-only", 7); + run.put_artifact(&artifact_only_node, "logs/output.txt", b"hello") .await .unwrap(); @@ -1217,13 +1217,13 @@ mod tests { vec![plan_blob, summary_blob] ); assert_eq!( - run.list_all_assets().await.unwrap(), + run.list_all_artifacts().await.unwrap(), vec![ - crate::slate::NodeAsset { + crate::slate::NodeArtifact { node: crate::StageId::new("artifact-only", 7), filename: "logs/output.txt".to_string(), }, - crate::slate::NodeAsset { + crate::slate::NodeArtifact { node: crate::StageId::new("code", 2), filename: "src/lib.rs".to_string(), } @@ -1470,7 +1470,7 @@ mod tests { .await .unwrap(); let summary_blob = run.write_blob(br#"{"done":true}"#).await.unwrap(); - run.put_asset(&node, "src/lib.rs", b"fn main() {}") + run.put_artifact(&node, "src/lib.rs", b"fn main() {}") .await .unwrap(); @@ -1515,7 +1515,7 @@ mod tests { Some(Bytes::from_static(br#"{"done":true}"#)) ); assert_eq!( - run.get_asset(&node, "src/lib.rs").await.unwrap(), + run.get_artifact(&node, "src/lib.rs").await.unwrap(), Some(Bytes::from_static(b"fn main() {}")) ); assert_eq!( diff --git a/lib/crates/fabro-store/src/slate/run_store.rs b/lib/crates/fabro-store/src/slate/run_store.rs index 4474fda2f..fe5b8a500 100644 --- a/lib/crates/fabro-store/src/slate/run_store.rs +++ b/lib/crates/fabro-store/src/slate/run_store.rs @@ -18,7 +18,7 @@ use crate::{EventEnvelope, EventPayload, Result, RunProjection, RunSummary, Stag use fabro_types::{RunBlobId, RunId}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct NodeAsset { +pub struct NodeArtifact { pub node: StageId, pub filename: String, } @@ -227,22 +227,22 @@ impl SlateRunStore { self.inner.db.list_blobs().await } - pub async fn put_asset(&self, node: &StageId, filename: &str, data: &[u8]) -> Result<()> { + pub async fn put_artifact(&self, node: &StageId, filename: &str, data: &[u8]) -> Result<()> { self.inner .db - .put_bytes(&keys::node_asset(node, filename), data) + .put_bytes(&keys::node_artifact(node, filename), data) .await } - pub async fn get_asset(&self, node: &StageId, filename: &str) -> Result> { + pub async fn get_artifact(&self, node: &StageId, filename: &str) -> Result> { self.inner .db - .get_bytes(&keys::node_asset(node, filename)) + .get_bytes(&keys::node_artifact(node, filename)) .await } - pub async fn list_all_assets(&self) -> Result> { - self.inner.db.list_all_assets().await + pub async fn list_all_artifacts(&self) -> Result> { + self.inner.db.list_all_artifacts().await } pub async fn state(&self) -> Result { @@ -294,10 +294,10 @@ impl SlateRunDb { } } - async fn list_all_assets(&self) -> Result> { + async fn list_all_artifacts(&self) -> Result> { match self { - Self::Writer(db) => list_all_assets(db).await, - Self::Reader(db) => list_all_assets(db.as_ref()).await, + Self::Writer(db) => list_all_artifacts(db).await, + Self::Reader(db) => list_all_artifacts(db.as_ref()).await, } } } @@ -383,7 +383,7 @@ where Ok(blob_ids) } -async fn list_all_assets(db: &R) -> Result> +async fn list_all_artifacts(db: &R) -> Result> where R: DbRead + Sync, { @@ -393,10 +393,10 @@ where let mut assets = Vec::new(); while let Some(entry) = iter.next().await? { let key = key_to_string(&entry.key)?; - let Some((node, filename)) = keys::parse_node_asset_key(&key) else { + let Some((node, filename)) = keys::parse_node_artifact_key(&key) else { continue; }; - assets.push(NodeAsset { node, filename }); + assets.push(NodeArtifact { node, filename }); } assets.sort(); Ok(assets) diff --git a/lib/crates/fabro-types/src/run_event/misc.rs b/lib/crates/fabro-types/src/run_event/misc.rs index 7016a8296..d4ab4d9e5 100644 --- a/lib/crates/fabro-types/src/run_event/misc.rs +++ b/lib/crates/fabro-types/src/run_event/misc.rs @@ -131,7 +131,7 @@ pub struct StallWatchdogTimeoutProps { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct AssetCapturedProps { +pub struct ArtifactCapturedProps { pub attempt: u32, pub node_slug: String, pub path: String, diff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs index aa9bbd22e..ec7d76f46 100644 --- a/lib/crates/fabro-types/src/run_event/mod.rs +++ b/lib/crates/fabro-types/src/run_event/mod.rs @@ -6,6 +6,7 @@ pub mod stage; use chrono::{DateTime, Utc}; use serde::de::Error as DeError; +use serde::ser::Error as SerError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{Map, Value, json}; @@ -56,6 +57,7 @@ pub struct RunEvent { pub body: EventBody, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(tag = "event", content = "properties")] pub enum EventBody { @@ -217,8 +219,8 @@ pub enum EventBody { SetupFailed(SetupFailedProps), #[serde(rename = "watchdog.timeout")] StallWatchdogTimeout(StallWatchdogTimeoutProps), - #[serde(rename = "asset.captured")] - AssetCaptured(AssetCapturedProps), + #[serde(rename = "artifact.captured")] + ArtifactCaptured(ArtifactCapturedProps), #[serde(rename = "ssh.ready")] SshAccessReady(SshAccessReadyProps), #[serde(rename = "agent.failover")] @@ -377,7 +379,7 @@ impl Serialize for RunEvent { S: Serializer, { self.to_value() - .map_err(serde::ser::Error::custom)? + .map_err(S::Error::custom)? .serialize(serializer) } } diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index 6ec1fd222..5692dabb0 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -18,7 +18,7 @@ pub use mcp::{ }; pub use project::ProjectSettings; pub use run::{ - AssetsSettings, CheckpointSettings, GitHubSettings, LlmSettings, MergeStrategy, + ArtifactsSettings, CheckpointSettings, GitHubSettings, LlmSettings, MergeStrategy, PullRequestSettings, SetupSettings, }; pub use sandbox::{ @@ -65,7 +65,7 @@ pub struct Settings { #[serde(default, skip_serializing_if = "Option::is_none")] pub pull_request: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub assets: Option, + pub artifacts: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub hooks: Vec, #[serde(default, skip_serializing_if = "HashMap::is_empty")] diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index 1fbca97c4..a46ad0b7e 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -34,7 +34,7 @@ pub enum MergeStrategy { } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct AssetsSettings { +pub struct ArtifactsSettings { #[serde(default)] pub include: Vec, } diff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs index 1899aae68..547a9c622 100644 --- a/lib/crates/fabro-workflow/src/artifact.rs +++ b/lib/crates/fabro-workflow/src/artifact.rs @@ -1,225 +1,50 @@ use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::RwLock; +use std::path::Path; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; use serde_json::Value; use fabro_agent::Sandbox; +use fabro_store::SlateRunStore; use crate::error::{FabroError, Result}; -/// Threshold above which artifacts are stored on disk instead of in memory (100KB). -const FILE_BACKING_THRESHOLD: usize = 100 * 1024; - -/// Metadata about a stored artifact. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ArtifactInfo { - pub id: String, - pub name: String, - pub size_bytes: usize, - pub stored_at: DateTime, - pub is_file_backed: bool, - pub file_path: Option, -} - -/// Storage for artifacts, either held in memory or backed by files on disk. -enum StoredData { - InMemory(Value), - FileBacked(PathBuf), -} - -/// Named, typed storage for large stage outputs. -pub struct ArtifactStore { - values_dir: Option, - artifacts: RwLock>, -} - -impl std::fmt::Debug for ArtifactStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ArtifactStore") - .field("values_dir", &self.values_dir) - .finish_non_exhaustive() - } -} - -impl ArtifactStore { - #[must_use] - pub fn new(values_dir: Option) -> Self { - Self { - values_dir, - artifacts: RwLock::new(HashMap::new()), - } - } - - /// Store an artifact. Large artifacts with a configured `values_dir` are written to disk. - /// - /// # Errors - /// - /// Returns an error if serialization fails or the file cannot be written. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - pub fn store( - &self, - id: impl Into, - name: impl Into, - data: Value, - ) -> Result { - let id = id.into(); - let name = name.into(); - let serialized = serde_json::to_string(&data) - .map_err(|e| FabroError::engine(format!("artifact serialize failed: {e}")))?; - let size_bytes = serialized.len(); - - let is_file_backed = size_bytes > FILE_BACKING_THRESHOLD && self.values_dir.is_some(); - - let (stored, file_path) = if is_file_backed { - let values_dir = self.values_dir.as_ref().expect("values_dir checked above"); - std::fs::create_dir_all(values_dir)?; - let path = values_dir.join(format!("{id}.json")); - std::fs::write(&path, &serialized)?; - (StoredData::FileBacked(path.clone()), Some(path)) - } else { - (StoredData::InMemory(data), None) - }; - - let info = ArtifactInfo { - id: id.clone(), - name, - size_bytes, - stored_at: Utc::now(), - is_file_backed, - file_path, - }; - - self.artifacts - .write() - .expect("artifact lock poisoned") - .insert(id, (info.clone(), stored)); - - Ok(info) - } - - /// Retrieve an artifact's data by ID. - /// - /// # Errors - /// - /// Returns an error if the artifact is not found or cannot be read from disk. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - pub fn retrieve(&self, id: &str) -> Result { - let guard = self.artifacts.read().expect("artifact lock poisoned"); - let (_, stored) = guard - .get(id) - .ok_or_else(|| FabroError::engine(format!("artifact not found: {id}")))?; - - match stored { - StoredData::InMemory(v) => Ok(v.clone()), - StoredData::FileBacked(path) => { - let path = path.clone(); - drop(guard); - let data = std::fs::read_to_string(&path).map_err(|e| { - FabroError::engine(format!("failed to read file-backed artifact {id}: {e}")) - })?; - serde_json::from_str(&data).map_err(|e| { - FabroError::engine(format!( - "failed to deserialize file-backed artifact {id}: {e}" - )) - }) - } - } - } - - /// Check if an artifact exists. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - pub fn has(&self, id: &str) -> bool { - self.artifacts - .read() - .expect("artifact lock poisoned") - .contains_key(id) - } - - /// List all artifact metadata. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - #[must_use] - pub fn list(&self) -> Vec { - self.artifacts - .read() - .expect("artifact lock poisoned") - .values() - .map(|(info, _)| info.clone()) - .collect() - } - - /// Remove an artifact by ID. Also deletes file-backed data from disk. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - pub fn remove(&self, id: &str) { - let mut guard = self.artifacts.write().expect("artifact lock poisoned"); - if let Some((_, StoredData::FileBacked(path))) = guard.remove(id) { - let _ = std::fs::remove_file(path); - } - } - - /// Returns the configured values directory. - /// Returns `None` if no file-backed storage is configured. - #[must_use] - pub fn values_dir(&self) -> Option { - self.values_dir.clone() - } - - /// Remove all artifacts. Also deletes file-backed data from disk. - /// - /// # Panics - /// - /// Panics if the internal lock is poisoned. - pub fn clear(&self) { - let mut guard = self.artifacts.write().expect("artifact lock poisoned"); - for (_, stored) in guard.values() { - if let StoredData::FileBacked(path) = stored { - let _ = std::fs::remove_file(path); - } - } - guard.clear(); - } -} +/// Threshold above which values are persisted as blobs and materialized to disk (100KB). +const BLOB_OFFLOAD_THRESHOLD: usize = 100 * 1024; /// Prefix used to identify artifact pointer strings in context values. const ARTIFACT_POINTER_PREFIX: &str = "file://"; -/// Offload context values exceeding the file-backing threshold into the artifact store. +/// Offload context values exceeding the blob threshold into SlateDB and materialize cache files. /// -/// For each entry in `updates` whose serialized JSON exceeds `FILE_BACKING_THRESHOLD`, -/// the value is stored in `store` and replaced with a `"file://{path}"` pointer. +/// For each entry in `updates` whose serialized JSON exceeds `BLOB_OFFLOAD_THRESHOLD`, +/// the value is persisted as a blob in `run_store`, materialized in `cache_dir`, and +/// replaced with a `"file://{path}"` pointer. /// Small values are left untouched. /// /// # Errors /// -/// Returns an error if storing an artifact fails. -pub fn offload_large_values( +/// Returns an error if blob persistence or cache materialization fails. +pub async fn offload_large_values( updates: &mut HashMap, - store: &ArtifactStore, + run_store: &SlateRunStore, + cache_dir: &Path, ) -> Result<()> { - for (key, value) in updates.iter_mut() { - let serialized_len = serde_json::to_string(&*value).map(|s| s.len()).unwrap_or(0); - if serialized_len > FILE_BACKING_THRESHOLD { - let info = store.store(key, key, value.clone())?; - if let Some(path) = info.file_path { - *value = Value::String(format!("{ARTIFACT_POINTER_PREFIX}{}", path.display())); + std::fs::create_dir_all(cache_dir)?; + + for value in updates.values_mut() { + let bytes = serde_json::to_vec(&*value) + .map_err(|e| FabroError::engine(format!("artifact serialize failed: {e}")))?; + + if bytes.len() > BLOB_OFFLOAD_THRESHOLD { + let blob_id = run_store + .write_blob(&bytes) + .await + .map_err(|e| FabroError::engine(format!("artifact blob write failed: {e}")))?; + let cache_path = cache_dir.join(format!("{blob_id}.json")); + if !cache_path.exists() { + std::fs::write(&cache_path, &bytes)?; } + *value = Value::String(format!("{ARTIFACT_POINTER_PREFIX}{}", cache_path.display())); } } Ok(()) @@ -303,160 +128,81 @@ pub async fn sync_artifacts_to_env( #[cfg(test)] mod tests { + use std::sync::Arc; + use std::time::Duration; + use super::*; + use fabro_store::SlateStore; + use object_store::memory::InMemory; + use ulid::Ulid; - #[test] - fn store_and_retrieve_small_artifact() { - let store = ArtifactStore::new(None); - let data = serde_json::json!({"result": "ok"}); - let info = store.store("art1", "test artifact", data.clone()).unwrap(); - - assert_eq!(info.id, "art1"); - assert_eq!(info.name, "test artifact"); - assert!(!info.is_file_backed); - assert!(info.size_bytes > 0); - assert!(info.file_path.is_none()); - - let retrieved = store.retrieve("art1").unwrap(); - assert_eq!(retrieved, data); + fn test_run_id(label: &str) -> fabro_types::RunId { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + use std::hash::{Hash, Hasher}; + label.hash(&mut hasher); + fabro_types::RunId::from(Ulid(u128::from(hasher.finish()))) } - #[test] - fn retrieve_nonexistent() { - let store = ArtifactStore::new(None); - assert!(store.retrieve("missing").is_err()); + async fn make_run_store(label: &str) -> fabro_store::SlateRunStore { + let object_store = Arc::new(InMemory::new()); + let store = SlateStore::new(object_store, "runs/", Duration::from_millis(1)); + store.create_run(&test_run_id(label)).await.unwrap() } - #[test] - fn has_artifact() { - let store = ArtifactStore::new(None); - assert!(!store.has("x")); - store.store("x", "x", serde_json::json!(1)).unwrap(); - assert!(store.has("x")); - } - - #[test] - fn list_artifacts() { - let store = ArtifactStore::new(None); - store.store("a", "alpha", serde_json::json!(1)).unwrap(); - store.store("b", "beta", serde_json::json!(2)).unwrap(); - let list = store.list(); - assert_eq!(list.len(), 2); - } - - #[test] - fn remove_artifact() { - let store = ArtifactStore::new(None); - store.store("r", "remove me", serde_json::json!(1)).unwrap(); - assert!(store.has("r")); - store.remove("r"); - assert!(!store.has("r")); - } - - #[test] - fn clear_artifacts() { - let store = ArtifactStore::new(None); - store.store("a", "a", serde_json::json!(1)).unwrap(); - store.store("b", "b", serde_json::json!(2)).unwrap(); - assert_eq!(store.list().len(), 2); - store.clear(); - assert!(store.list().is_empty()); - } - - #[test] - fn file_backed_storage() { + #[tokio::test] + async fn offload_replaces_large_values_with_blob_backed_pointer() { let dir = tempfile::tempdir().unwrap(); - let store = ArtifactStore::new(Some(dir.path().to_path_buf())); + let run_store = make_run_store("artifact-offload").await; - // Create data larger than the 100KB threshold - let large_string = "x".repeat(FILE_BACKING_THRESHOLD + 1); - let data = serde_json::json!(large_string); + let large_string = "x".repeat(BLOB_OFFLOAD_THRESHOLD + 1); + let serialized = serde_json::to_vec(&serde_json::json!(large_string.clone())).unwrap(); + let expected_blob_id = + fabro_types::RunBlobId::new(&test_run_id("artifact-offload"), &serialized); - let info = store.store("big", "large artifact", data.clone()).unwrap(); - assert!(info.is_file_backed); - assert!(info.size_bytes > FILE_BACKING_THRESHOLD); - assert_eq!(info.file_path, Some(dir.path().join("big.json"))); - - let retrieved = store.retrieve("big").unwrap(); - assert_eq!(retrieved, data); - } - - #[test] - fn file_backed_remove_deletes_file() { - let dir = tempfile::tempdir().unwrap(); - let store = ArtifactStore::new(Some(dir.path().to_path_buf())); - - let large_string = "x".repeat(FILE_BACKING_THRESHOLD + 1); - let data = serde_json::json!(large_string); - store.store("big", "large", data).unwrap(); - - let file_path = dir.path().join("big.json"); - assert!(file_path.exists()); - - store.remove("big"); - assert!(!file_path.exists()); - } - - #[test] - fn small_artifact_stays_in_memory_even_with_values_dir() { - let dir = tempfile::tempdir().unwrap(); - let store = ArtifactStore::new(Some(dir.path().to_path_buf())); - - let data = serde_json::json!({"small": true}); - let info = store.store("small", "tiny", data).unwrap(); - assert!(!info.is_file_backed); - } - - #[test] - fn no_file_backing_without_base_dir() { - let store = ArtifactStore::new(None); - - let large_string = "x".repeat(FILE_BACKING_THRESHOLD + 1); - let data = serde_json::json!(large_string); - let info = store.store("big", "large", data).unwrap(); - assert!(!info.is_file_backed); - } - - #[test] - fn offload_replaces_large_values_with_pointer() { - let dir = tempfile::tempdir().unwrap(); - let store = ArtifactStore::new(Some(dir.path().to_path_buf())); - - let large_string = "x".repeat(FILE_BACKING_THRESHOLD + 1); let mut updates = HashMap::new(); updates.insert("response.plan".to_string(), serde_json::json!(large_string)); - offload_large_values(&mut updates, &store).unwrap(); + offload_large_values(&mut updates, &run_store, dir.path()) + .await + .unwrap(); - // Value should now be a pointer string let pointer = updates.get("response.plan").unwrap(); let path = artifact_path(pointer).expect("should be an artifact pointer"); assert_eq!( path, - dir.path().join("response.plan.json").to_str().unwrap() + dir.path() + .join(format!("{expected_blob_id}.json")) + .to_str() + .unwrap() ); - // The artifact store should contain the original value - let retrieved = store.retrieve("response.plan").unwrap(); - assert_eq!(retrieved, serde_json::json!(large_string)); - - // File should exist on disk - assert!(dir.path().join("response.plan.json").exists()); + let blob = run_store + .read_blob(&expected_blob_id) + .await + .unwrap() + .expect("blob should exist"); + let blob_value: serde_json::Value = serde_json::from_slice(&blob).unwrap(); + assert_eq!(blob_value, serde_json::json!(large_string)); + assert!( + dir.path().join(format!("{expected_blob_id}.json")).exists(), + "materialized cache file should exist" + ); } - #[test] - fn offload_leaves_small_values_untouched() { + #[tokio::test] + async fn offload_leaves_small_values_untouched() { let dir = tempfile::tempdir().unwrap(); - let store = ArtifactStore::new(Some(dir.path().to_path_buf())); - + let run_store = make_run_store("artifact-small").await; let small_value = serde_json::json!("hello world"); let mut updates = HashMap::new(); updates.insert("small_key".to_string(), small_value.clone()); - offload_large_values(&mut updates, &store).unwrap(); + offload_large_values(&mut updates, &run_store, dir.path()) + .await + .unwrap(); assert_eq!(updates.get("small_key").unwrap(), &small_value); - assert!(!store.has("small_key")); + assert!(std::fs::read_dir(dir.path()).unwrap().next().is_none()); } #[test] @@ -468,14 +214,6 @@ mod tests { ); } - #[test] - fn values_dir_returns_configured_directory() { - let dir = tempfile::tempdir().unwrap(); - let store = ArtifactStore::new(Some(dir.path().to_path_buf())); - - assert_eq!(store.values_dir(), Some(dir.path().to_path_buf())); - } - #[test] fn artifact_path_returns_none_for_plain_string() { let value = serde_json::json!("just a normal string"); diff --git a/lib/crates/fabro-workflow/src/asset_snapshot.rs b/lib/crates/fabro-workflow/src/artifact_snapshot.rs similarity index 89% rename from lib/crates/fabro-workflow/src/asset_snapshot.rs rename to lib/crates/fabro-workflow/src/artifact_snapshot.rs index ac1ee4380..ebef71da0 100644 --- a/lib/crates/fabro-workflow/src/asset_snapshot.rs +++ b/lib/crates/fabro-workflow/src/artifact_snapshot.rs @@ -13,9 +13,9 @@ pub struct DiscoveredFile { pub mtime_epoch_secs: f64, } -/// Metadata for a single captured asset file. +/// Metadata for a single captured artifact file. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CapturedAssetInfo { +pub struct CapturedArtifactInfo { pub path: String, pub mime: String, pub content_md5: String, @@ -23,15 +23,15 @@ pub struct CapturedAssetInfo { pub bytes: u64, } -/// Summary of an asset collection run. +/// Summary of an artifact collection run. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AssetCollectionSummary { +pub struct ArtifactCollectionSummary { pub files_copied: usize, pub total_bytes: u64, pub files_skipped: usize, pub download_errors: usize, pub hash_errors: usize, - pub captured_assets: Vec, + pub captured_assets: Vec, } /// Directories to exclude from the find search and checkpoint commits. @@ -61,7 +61,7 @@ const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; /// Maximum total size for all collected files (50 MB). const MAX_TOTAL_SIZE: u64 = 50 * 1024 * 1024; -/// Build a platform-aware find command to discover asset files matching the given globs. +/// Build a platform-aware find command to discover artifact files matching the given globs. /// /// Globs without `/` are treated as filename patterns (`-name`). /// Globs with `/` are treated as directory patterns: the trailing `/**` (if any) is stripped @@ -254,10 +254,10 @@ fn normalize_paths(discovered: Vec, root: &str) -> Vec std::result::Result { +) -> std::result::Result { let mime = mime_guess::from_path(relative_path) .first_or_octet_stream() .to_string(); @@ -266,7 +266,7 @@ fn compute_asset_info( let bytes = u64::try_from(data.len()).unwrap_or(u64::MAX); let content_md5 = format!("{:x}", md5::compute(&data)); let content_sha256 = hex::encode(Sha256::digest(&data)); - Ok(CapturedAssetInfo { + Ok(CapturedArtifactInfo { path: relative_path.to_string(), mime, content_md5, @@ -275,13 +275,13 @@ fn compute_asset_info( }) } -fn write_asset_manifest( - asset_capture_dir: &Path, - summary: &AssetCollectionSummary, +fn write_artifact_manifest( + artifact_capture_dir: &Path, + summary: &ArtifactCollectionSummary, ) -> Result<(), String> { let json = serde_json::to_string_pretty(summary) .map_err(|e| format!("failed to serialize manifest: {e}"))?; - let manifest_path = asset_capture_dir.join("manifest.json"); + let manifest_path = artifact_capture_dir.join("manifest.json"); if let Some(parent) = manifest_path.parent() { std::fs::create_dir_all(parent).map_err(|e| { format!( @@ -295,26 +295,26 @@ fn write_asset_manifest( Ok(()) } -fn cleanup_asset_capture_dir(asset_capture_dir: &Path) -> Result<(), String> { - if !asset_capture_dir.exists() { +fn cleanup_artifact_capture_dir(artifact_capture_dir: &Path) -> Result<(), String> { + if !artifact_capture_dir.exists() { return Ok(()); } - std::fs::remove_dir_all(asset_capture_dir) - .map_err(|e| format!("failed to clean up {}: {e}", asset_capture_dir.display())) + std::fs::remove_dir_all(artifact_capture_dir) + .map_err(|e| format!("failed to clean up {}: {e}", artifact_capture_dir.display())) } -/// Collect asset files matching the configured globs that were created during this stage. -pub async fn collect_assets( +/// Collect artifact files matching the configured globs that were created during this stage. +pub async fn collect_artifacts( sandbox: &dyn Sandbox, - asset_capture_dir: &Path, + artifact_capture_dir: &Path, globs: &[String], command_start_epoch: f64, -) -> Result { +) -> Result { let root = sandbox.working_directory(); let platform = sandbox.platform(); let cmd = build_find_command(root, platform, globs); - debug!(cmd = cmd.as_str(), "Collecting assets"); + debug!(cmd = cmd.as_str(), "Collecting artifacts"); let result = sandbox .exec_command(&cmd, FIND_TIMEOUT_MS, None, None, None) .await?; @@ -330,15 +330,15 @@ pub async fn collect_assets( let mut total_bytes: u64 = 0; let mut download_errors: usize = 0; let mut hash_errors: usize = 0; - let mut captured_assets: Vec = Vec::new(); + let mut captured_assets: Vec = Vec::new(); for file in &to_collect { - let dest = asset_capture_dir.join(&file.relative_path); + let dest = artifact_capture_dir.join(&file.relative_path); match sandbox .download_file_to_local(&file.relative_path, &dest) .await { - Ok(()) => match compute_asset_info(&file.relative_path, &dest) { + Ok(()) => match compute_artifact_info(&file.relative_path, &dest) { Ok(info) => { files_copied += 1; total_bytes += info.bytes; @@ -366,7 +366,7 @@ pub async fn collect_assets( } // Write manifest.json - let summary = AssetCollectionSummary { + let summary = ArtifactCollectionSummary { files_copied, total_bytes, files_skipped, @@ -376,8 +376,8 @@ pub async fn collect_assets( }; if files_copied > 0 { - if let Err(e) = write_asset_manifest(asset_capture_dir, &summary) { - let cleanup_suffix = match cleanup_asset_capture_dir(asset_capture_dir) { + if let Err(e) = write_artifact_manifest(artifact_capture_dir, &summary) { + let cleanup_suffix = match cleanup_artifact_capture_dir(artifact_capture_dir) { Ok(()) => String::new(), Err(cleanup_err) => format!("; cleanup failed: {cleanup_err}"), }; @@ -388,11 +388,11 @@ pub async fn collect_assets( Ok(summary) } -/// Collect all asset paths from manifest files under `{assets_dir}/*/retry_*/manifest.json`. +/// Collect all artifact paths from manifest files under `{artifacts_dir}/*/retry_*/manifest.json`. /// -/// Returns the full on-disk paths to the downloaded asset files. -pub fn collect_asset_paths(assets_dir: &Path) -> Vec { - let Ok(nodes) = std::fs::read_dir(assets_dir) else { +/// Returns the full on-disk paths to the downloaded artifact files. +pub fn collect_artifact_paths(artifacts_dir: &Path) -> Vec { + let Ok(nodes) = std::fs::read_dir(artifacts_dir) else { return Vec::new(); }; @@ -409,7 +409,7 @@ pub fn collect_asset_paths(assets_dir: &Path) -> Vec { let Ok(contents) = std::fs::read_to_string(&manifest) else { continue; }; - let Ok(summary) = serde_json::from_str::(&contents) else { + let Ok(summary) = serde_json::from_str::(&contents) else { continue; }; let retry_dir = retry_entry.path(); @@ -429,7 +429,7 @@ mod tests { use std::collections::HashMap; use std::fs; - /// Minimal mock sandbox for asset_snapshot tests. + /// Minimal mock sandbox for artifact_snapshot tests. struct AssetMockSandbox { files: HashMap, exec_result: ExecResult, @@ -718,7 +718,7 @@ mod tests { let mock = AssetMockSandbox::new(files, "1024\t2000.0\ttest-results/r.xml\n", "linux"); let globs = vec!["test-results/**".to_string()]; - let summary = collect_assets(&mock, stage_dir.path(), &globs, 1000.0) + let summary = collect_artifacts(&mock, stage_dir.path(), &globs, 1000.0) .await .unwrap(); @@ -759,7 +759,7 @@ mod tests { let mock = AssetMockSandbox::new(files, "1024\t500.0\ttest-results/r.xml\n", "linux"); let globs = vec!["test-results/**".to_string()]; - let summary = collect_assets(&mock, stage_dir.path(), &globs, 1000.0) + let summary = collect_artifacts(&mock, stage_dir.path(), &globs, 1000.0) .await .unwrap(); @@ -778,7 +778,7 @@ mod tests { ); let globs = vec!["test-results/**".to_string()]; - let summary = collect_assets(&mock, stage_dir.path(), &globs, 1000.0) + let summary = collect_artifacts(&mock, stage_dir.path(), &globs, 1000.0) .await .unwrap(); @@ -789,7 +789,7 @@ mod tests { #[cfg(unix)] #[test] - fn write_asset_manifest_failure_cleans_up_asset_capture_dir() { + fn write_artifact_manifest_failure_cleans_up_artifact_capture_dir() { use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; @@ -799,13 +799,13 @@ mod tests { fs::write(stage_dir.join("test-results/report.xml"), "").unwrap(); fs::set_permissions(&stage_dir, Permissions::from_mode(0o555)).unwrap(); - let summary = AssetCollectionSummary { + let summary = ArtifactCollectionSummary { files_copied: 1, total_bytes: 7, files_skipped: 0, download_errors: 0, hash_errors: 0, - captured_assets: vec![CapturedAssetInfo { + captured_assets: vec![CapturedArtifactInfo { path: "test-results/report.xml".to_string(), mime: "text/xml".to_string(), content_md5: "f1430934c390c118ed2f148e1d44d36c".to_string(), @@ -815,40 +815,40 @@ mod tests { }], }; - let err = write_asset_manifest(&stage_dir, &summary).unwrap_err(); + let err = write_artifact_manifest(&stage_dir, &summary).unwrap_err(); assert!(err.contains("failed to write")); fs::set_permissions(&stage_dir, Permissions::from_mode(0o755)).unwrap(); - cleanup_asset_capture_dir(&stage_dir).unwrap(); + cleanup_artifact_capture_dir(&stage_dir).unwrap(); assert!(!stage_dir.exists()); } #[test] - fn collect_asset_paths_from_manifests() { + fn collect_artifact_paths_from_manifests() { let tmp = tempfile::tempdir().unwrap(); let base = tmp.path(); - let assets_dir = base.join("cache/artifacts/assets"); + let artifacts_dir = base.join("cache/artifacts/files"); // Create two node directories with manifests - let node_a = assets_dir.join("node_a/retry_1"); + let node_a = artifacts_dir.join("node_a/retry_1"); std::fs::create_dir_all(&node_a).unwrap(); std::fs::write( node_a.join("manifest.json"), - serde_json::to_string(&AssetCollectionSummary { + serde_json::to_string(&ArtifactCollectionSummary { files_copied: 2, total_bytes: 2048, files_skipped: 0, download_errors: 0, hash_errors: 0, captured_assets: vec![ - CapturedAssetInfo { + CapturedArtifactInfo { path: "test-results/report.xml".to_string(), mime: "text/xml".to_string(), content_md5: "md5-report".to_string(), content_sha256: "sha256-report".to_string(), bytes: 1024, }, - CapturedAssetInfo { + CapturedArtifactInfo { path: "test-results/screenshot.png".to_string(), mime: "image/png".to_string(), content_md5: "md5-screenshot".to_string(), @@ -861,17 +861,17 @@ mod tests { ) .unwrap(); - let node_b = assets_dir.join("node_b/retry_1"); + let node_b = artifacts_dir.join("node_b/retry_1"); std::fs::create_dir_all(&node_b).unwrap(); std::fs::write( node_b.join("manifest.json"), - serde_json::to_string(&AssetCollectionSummary { + serde_json::to_string(&ArtifactCollectionSummary { files_copied: 1, total_bytes: 512, files_skipped: 0, download_errors: 0, hash_errors: 0, - captured_assets: vec![CapturedAssetInfo { + captured_assets: vec![CapturedArtifactInfo { path: "coverage/lcov.info".to_string(), mime: "application/octet-stream".to_string(), content_md5: "md5-lcov".to_string(), @@ -883,24 +883,24 @@ mod tests { ) .unwrap(); - let paths = collect_asset_paths(&assets_dir); + let paths = collect_artifact_paths(&artifacts_dir); assert_eq!(paths.len(), 3); let base_str = base.to_string_lossy(); assert!(paths.contains(&format!( - "{base_str}/cache/artifacts/assets/node_a/retry_1/test-results/report.xml" + "{base_str}/cache/artifacts/files/node_a/retry_1/test-results/report.xml" ))); assert!(paths.contains(&format!( - "{base_str}/cache/artifacts/assets/node_a/retry_1/test-results/screenshot.png" + "{base_str}/cache/artifacts/files/node_a/retry_1/test-results/screenshot.png" ))); assert!(paths.contains(&format!( - "{base_str}/cache/artifacts/assets/node_b/retry_1/coverage/lcov.info" + "{base_str}/cache/artifacts/files/node_b/retry_1/coverage/lcov.info" ))); } #[test] fn collect_asset_paths_empty_when_no_assets() { let tmp = tempfile::tempdir().unwrap(); - let paths = collect_asset_paths(&tmp.path().join("cache/artifacts/assets")); + let paths = collect_artifact_paths(&tmp.path().join("cache/artifacts/files")); assert!(paths.is_empty()); } diff --git a/lib/crates/fabro-workflow/src/assets.rs b/lib/crates/fabro-workflow/src/artifacts.rs similarity index 76% rename from lib/crates/fabro-workflow/src/assets.rs rename to lib/crates/fabro-workflow/src/artifacts.rs index 7b7016dc8..4dd83a491 100644 --- a/lib/crates/fabro-workflow/src/assets.rs +++ b/lib/crates/fabro-workflow/src/artifacts.rs @@ -2,11 +2,11 @@ use std::path::{Path, PathBuf}; use anyhow::Result; -use crate::asset_snapshot::AssetCollectionSummary; +use crate::artifact_snapshot::ArtifactCollectionSummary; -/// An individual asset file discovered from a run's asset manifests. +/// An individual artifact file discovered from a run's artifact manifests. #[derive(Debug, Clone, serde::Serialize)] -pub struct AssetEntry { +pub struct ArtifactEntry { pub node_slug: String, pub retry: u32, pub relative_path: String, @@ -19,13 +19,13 @@ fn serialize_path(path: &Path, serializer: S) -> Result, retry_filter: Option, -) -> Result> { - let Ok(nodes) = std::fs::read_dir(assets_dir) else { +) -> Result> { + let Ok(nodes) = std::fs::read_dir(artifacts_dir) else { return Ok(Vec::new()); }; @@ -63,13 +63,13 @@ pub fn scan_assets( let Ok(contents) = std::fs::read_to_string(&manifest) else { continue; }; - let Ok(summary) = serde_json::from_str::(&contents) else { + let Ok(summary) = serde_json::from_str::(&contents) else { continue; }; for asset in &summary.captured_assets { let absolute_path = retry_dir.join(&asset.path); - entries.push(AssetEntry { + entries.push(ArtifactEntry { node_slug: node_slug.clone(), retry, relative_path: asset.path.clone(), @@ -93,24 +93,24 @@ pub fn scan_assets( #[cfg(test)] mod tests { use super::*; - use crate::asset_snapshot::{AssetCollectionSummary, CapturedAssetInfo}; + use crate::artifact_snapshot::{ArtifactCollectionSummary, CapturedArtifactInfo}; #[test] - fn scan_assets_filters_by_node_and_retry() { + fn scan_artifacts_filters_by_node_and_retry() { let tmp = tempfile::tempdir().unwrap(); - let assets_dir = tmp.path().join("cache/artifacts/assets"); + let artifacts_dir = tmp.path().join("cache/artifacts/files"); - let retry_1 = assets_dir.join("work/retry_1"); + let retry_1 = artifacts_dir.join("work/retry_1"); std::fs::create_dir_all(&retry_1).unwrap(); std::fs::write( retry_1.join("manifest.json"), - serde_json::to_string(&AssetCollectionSummary { + serde_json::to_string(&ArtifactCollectionSummary { files_copied: 1, total_bytes: 5, files_skipped: 0, download_errors: 0, hash_errors: 0, - captured_assets: vec![CapturedAssetInfo { + captured_assets: vec![CapturedArtifactInfo { path: "report.txt".to_string(), mime: "text/plain".to_string(), content_md5: "a".repeat(32), @@ -122,17 +122,17 @@ mod tests { ) .unwrap(); - let retry_2 = assets_dir.join("work/retry_2"); + let retry_2 = artifacts_dir.join("work/retry_2"); std::fs::create_dir_all(&retry_2).unwrap(); std::fs::write( retry_2.join("manifest.json"), - serde_json::to_string(&AssetCollectionSummary { + serde_json::to_string(&ArtifactCollectionSummary { files_copied: 1, total_bytes: 6, files_skipped: 0, download_errors: 0, hash_errors: 0, - captured_assets: vec![CapturedAssetInfo { + captured_assets: vec![CapturedArtifactInfo { path: "report.txt".to_string(), mime: "text/plain".to_string(), content_md5: "c".repeat(32), @@ -144,7 +144,7 @@ mod tests { ) .unwrap(); - let entries = scan_assets(&assets_dir, Some("work"), Some(2)).unwrap(); + let entries = scan_artifacts(&artifacts_dir, Some("work"), Some(2)).unwrap(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].retry, 2); assert_eq!(entries[0].relative_path, "report.txt"); diff --git a/lib/crates/fabro-workflow/src/event.rs b/lib/crates/fabro-workflow/src/event.rs index 5b63a4561..fb0e73dd2 100644 --- a/lib/crates/fabro-workflow/src/event.rs +++ b/lib/crates/fabro-workflow/src/event.rs @@ -365,7 +365,7 @@ pub enum Event { node: String, idle_seconds: u64, }, - AssetCaptured { + ArtifactCaptured { node_id: String, attempt: u32, node_slug: String, @@ -864,7 +864,7 @@ impl Event { Self::StallWatchdogTimeout { node, idle_seconds } => { warn!(node, idle_seconds, "Stall watchdog timeout"); } - Self::AssetCaptured { + Self::ArtifactCaptured { node_id, node_slug, attempt, @@ -872,7 +872,10 @@ impl Event { bytes, .. } => { - debug!(node_id, node_slug, attempt, path, bytes, "Asset captured"); + debug!( + node_id, + node_slug, attempt, path, bytes, "Artifact captured" + ); } Self::SshAccessReady { ssh_command } => { info!(ssh_command, "SSH access ready"); @@ -1146,7 +1149,7 @@ pub fn event_name(event: &Event) -> &'static str { Event::SetupCompleted { .. } => "setup.completed", Event::SetupFailed { .. } => "setup.failed", Event::StallWatchdogTimeout { .. } => "watchdog.timeout", - Event::AssetCaptured { .. } => "asset.captured", + Event::ArtifactCaptured { .. } => "artifact.captured", Event::SshAccessReady { .. } => "ssh.ready", Event::Failover { .. } => "agent.failover", Event::CliEnsureStarted { .. } => "cli.ensure.started", @@ -1264,7 +1267,7 @@ fn extract_run_event_fields(event: &Event) -> StoredEventFields { | Event::CheckpointFailed { .. } | Event::SubgraphStarted { .. } | Event::SubgraphCompleted { .. } - | Event::AssetCaptured { .. } + | Event::ArtifactCaptured { .. } | Event::PromptCompleted { .. } | Event::ParallelStarted { .. } | Event::ParallelCompleted { .. } diff --git a/lib/crates/fabro-workflow/src/lib.rs b/lib/crates/fabro-workflow/src/lib.rs index d01b92daf..351d38936 100644 --- a/lib/crates/fabro-workflow/src/lib.rs +++ b/lib/crates/fabro-workflow/src/lib.rs @@ -111,8 +111,8 @@ pub fn extract_stage_durations_from_events(events: &[EventEnvelope]) -> HashMap< #[doc(hidden)] pub mod artifact; -pub mod asset_snapshot; -pub mod assets; +pub mod artifact_snapshot; +pub mod artifacts; pub(crate) mod condition; pub mod context; pub mod devcontainer_bridge; diff --git a/lib/crates/fabro-workflow/src/lifecycle/artifact.rs b/lib/crates/fabro-workflow/src/lifecycle/artifact.rs index 199b29a7b..be3330d69 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/artifact.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/artifact.rs @@ -1,15 +1,17 @@ use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; use async_trait::async_trait; +use fabro_store::SlateRunStore; use fabro_core::graph::NodeSpec; use fabro_core::lifecycle::{AttemptContext, AttemptResultContext, RunLifecycle}; use fabro_core::outcome::NodeResult; use fabro_core::state::ExecutionState; -use crate::artifact::{ArtifactStore, offload_large_values, sync_artifacts_to_env}; -use crate::asset_snapshot::collect_assets; +use crate::artifact::{offload_large_values, sync_artifacts_to_env}; +use crate::artifact_snapshot::collect_artifacts; use crate::event::{Event, EventEmitter, RunNoticeLevel}; use crate::graph::WorkflowGraph; use crate::graph::WorkflowNode; @@ -24,33 +26,36 @@ type WfNodeDecision = NodeDecision>; /// Sub-lifecycle responsible for artifact collection, offloading, and syncing. pub(crate) struct ArtifactLifecycle { pub sandbox: Arc, - pub artifact_store: Arc>, - pub artifact_values_dir: Option, + pub run_store: SlateRunStore, + pub blob_cache_dir: PathBuf, pub emitter: Arc, - pub assets_dir: PathBuf, - pub asset_globs: Vec, + pub artifacts_dir: PathBuf, + pub artifact_globs: Vec, + pub captured_artifact_count: Arc, /// Per-attempt state: epoch seconds when the attempt started. - attempt_start_epoch: Mutex>, + attempt_start_epoch: std::sync::Mutex>, } impl ArtifactLifecycle { #[allow(clippy::too_many_arguments)] pub(crate) fn new( sandbox: Arc, - artifact_store: Arc>, - artifact_values_dir: Option, + run_store: SlateRunStore, + blob_cache_dir: PathBuf, emitter: Arc, - assets_dir: PathBuf, - asset_globs: Vec, + artifacts_dir: PathBuf, + artifact_globs: Vec, + captured_artifact_count: Arc, ) -> Self { Self { sandbox, - artifact_store, - artifact_values_dir, + run_store, + blob_cache_dir, emitter, - assets_dir, - asset_globs, - attempt_start_epoch: Mutex::new(None), + artifacts_dir, + artifact_globs, + captured_artifact_count, + attempt_start_epoch: std::sync::Mutex::new(None), } } } @@ -58,9 +63,7 @@ impl ArtifactLifecycle { #[async_trait] impl RunLifecycle for ArtifactLifecycle { async fn on_run_start(&self, _graph: &WorkflowGraph, _state: &WfRunState) -> CoreResult<()> { - // Swap in a fresh artifact store on restart (don't call clear() — preserves files on disk) - let mut store = self.artifact_store.lock().unwrap(); - *store = ArtifactStore::new(self.artifact_values_dir.clone()); + self.captured_artifact_count.store(0, Ordering::Relaxed); *self.attempt_start_epoch.lock().unwrap() = None; Ok(()) } @@ -84,7 +87,7 @@ impl RunLifecycle for ArtifactLifecycle { ctx: &AttemptResultContext<'_, WorkflowGraph>, state: &WfRunState, ) -> CoreResult<()> { - if self.asset_globs.is_empty() { + if self.artifact_globs.is_empty() { return Ok(()); } let epoch = self.attempt_start_epoch.lock().unwrap().unwrap_or(0.0); @@ -95,16 +98,24 @@ impl RunLifecycle for ArtifactLifecycle { } else { format!("{node_id}-visit_{visit}") }; - let asset_capture_dir = self - .assets_dir + let artifact_capture_dir = self + .artifacts_dir .join(&node_slug) .join(format!("retry_{}", ctx.attempt)); - let _ = std::fs::create_dir_all(&asset_capture_dir); + let _ = std::fs::create_dir_all(&artifact_capture_dir); - match collect_assets(&*self.sandbox, &asset_capture_dir, &self.asset_globs, epoch).await { + match collect_artifacts( + &*self.sandbox, + &artifact_capture_dir, + &self.artifact_globs, + epoch, + ) + .await + { Ok(summary) if summary.files_copied > 0 => { for asset in &summary.captured_assets { - self.emitter.emit(&Event::AssetCaptured { + self.captured_artifact_count.fetch_add(1, Ordering::Relaxed); + self.emitter.emit(&Event::ArtifactCaptured { node_id: node_id.to_string(), attempt: ctx.attempt, node_slug: node_slug.clone(), @@ -120,8 +131,8 @@ impl RunLifecycle for ArtifactLifecycle { Err(e) => { self.emitter.emit(&Event::RunNotice { level: RunNoticeLevel::Warn, - code: "asset_collection_failed".to_string(), - message: format!("[node: {node_id}] asset collection failed: {e}"), + code: "artifact_collection_failed".to_string(), + message: format!("[node: {node_id}] artifact collection failed: {e}"), }); } } @@ -138,15 +149,18 @@ impl RunLifecycle for ArtifactLifecycle { let node_id = node.id(); // Offload large context_updates values to artifact store + if let Err(e) = offload_large_values( + &mut result.outcome.context_updates, + &self.run_store, + &self.blob_cache_dir, + ) + .await { - let store = self.artifact_store.lock().unwrap(); - if let Err(e) = offload_large_values(&mut result.outcome.context_updates, &store) { - self.emitter.emit(&Event::RunNotice { - level: RunNoticeLevel::Warn, - code: "artifact_offload_failed".to_string(), - message: format!("[node: {node_id}] artifact offload failed: {e}"), - }); - } + self.emitter.emit(&Event::RunNotice { + level: RunNoticeLevel::Warn, + code: "artifact_offload_failed".to_string(), + message: format!("[node: {node_id}] artifact offload failed: {e}"), + }); } // Sync file-backed artifacts to sandbox environment diff --git a/lib/crates/fabro-workflow/src/lifecycle/event.rs b/lib/crates/fabro-workflow/src/lifecycle/event.rs index 6bf2a176d..cbc111ba2 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/event.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/event.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -14,7 +15,6 @@ use fabro_core::state::ExecutionState; use super::circuit_breaker::CircuitBreakerLifecycle; use super::git::GitCheckpointResult; -use crate::artifact::ArtifactStore; use crate::context; use crate::error::FabroError; use crate::event::{Event, EventEmitter}; @@ -46,8 +46,7 @@ pub(crate) struct EventLifecycle { pub run_branch: Option, pub worktree_dir: Option, pub goal: Option, - // Shared swappable handle (same instance as orchestrator) - pub artifact_store: Arc>, + pub captured_artifact_count: Arc, // Cross-lifecycle data pub checkpoint_git_result: Arc>>, pub last_git_sha: Arc>>, @@ -385,7 +384,7 @@ impl RunLifecycle for EventLifecycle { } let duration_ms = u64::try_from(self.run_start.lock().unwrap().elapsed().as_millis()).unwrap(); - let artifact_count = self.artifact_store.lock().unwrap().list().len(); + let artifact_count = self.captured_artifact_count.load(Ordering::Relaxed); let last_sha = self.last_git_sha.lock().unwrap().clone(); let final_patch = self.final_patch.lock().unwrap().clone(); let total_cost = { diff --git a/lib/crates/fabro-workflow/src/lifecycle/git.rs b/lib/crates/fabro-workflow/src/lifecycle/git.rs index 022463915..492030bbf 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/git.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/git.rs @@ -13,7 +13,6 @@ use fabro_core::lifecycle::RunLifecycle; use fabro_core::outcome::NodeResult; use fabro_core::state::ExecutionState; -use crate::artifact::ArtifactStore; use crate::event::{Event, EventEmitter, RunNoticeLevel}; use crate::git::MetadataStore; use crate::graph::WorkflowGraph; @@ -64,7 +63,6 @@ pub(crate) struct GitCheckpointResult { /// Sub-lifecycle responsible for git operations (checkpoint commits, pushes, diffs). pub(crate) struct GitLifecycle { pub sandbox: Arc, - pub artifact_store: Arc>, pub emitter: Arc, pub run_dir: PathBuf, pub run_id: RunId, @@ -152,20 +150,7 @@ impl RunLifecycle for GitLifecycle { None, ); if let Ok(cp_json) = serde_json::to_vec_pretty(&checkpoint) { - let mut extra_entries: Vec<(String, Vec)> = { - let artifact_store = self.artifact_store.lock().unwrap(); - artifact_store - .list() - .iter() - .filter_map(|info| { - info.file_path.as_ref().and_then(|path| { - std::fs::read(path) - .ok() - .map(|data| (format!("artifacts/{}.json", info.id), data)) - }) - }) - .collect() - }; + let mut extra_entries: Vec<(String, Vec)> = Vec::new(); if let Ok(store_state) = self.run_store.state().await { if let Ok(mut dump_entries) = RunDump::metadata_checkpoint(&store_state).git_entries() diff --git a/lib/crates/fabro-workflow/src/lifecycle/mod.rs b/lib/crates/fabro-workflow/src/lifecycle/mod.rs index 7df1f6bce..7ec808f46 100644 --- a/lib/crates/fabro-workflow/src/lifecycle/mod.rs +++ b/lib/crates/fabro-workflow/src/lifecycle/mod.rs @@ -8,7 +8,7 @@ pub(crate) mod hook; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -25,7 +25,6 @@ use fabro_core::lifecycle::{ use fabro_core::outcome::NodeResult; use fabro_core::state::ExecutionState; -use crate::artifact::ArtifactStore; use crate::context; use crate::error::{FailureSignature, FailureSignatureExt}; use crate::event::EventEmitter; @@ -94,9 +93,7 @@ impl WorkflowLifecycle { Arc::new(Mutex::new(None)); let last_git_sha: Arc>> = Arc::new(Mutex::new(None)); let final_patch: Arc>> = Arc::new(Mutex::new(None)); - let artifact_store = Arc::new(Mutex::new(ArtifactStore::new(Some( - runtime_state.artifact_values_dir(), - )))); + let captured_artifact_count = Arc::new(AtomicUsize::new(0)); let circuit_breaker = Arc::new(CircuitBreakerLifecycle::new(loop_restart_signature_limit)); @@ -123,7 +120,7 @@ impl WorkflowLifecycle { 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()), - artifact_store: Arc::clone(&artifact_store), + captured_artifact_count: Arc::clone(&captured_artifact_count), last_git_sha: Arc::clone(&last_git_sha), final_patch: Arc::clone(&final_patch), checkpoint_git_result: Arc::clone(&checkpoint_git_result), @@ -144,11 +141,10 @@ impl WorkflowLifecycle { let git = GitLifecycle { sandbox: Arc::clone(sandbox), - artifact_store: Arc::clone(&artifact_store), emitter: Arc::clone(emitter), run_dir: run_dir.clone(), run_id: run_options.run_id, - run_store, + run_store: run_store.clone(), run_options: Arc::clone(run_options), start_node_id, checkpoint_git_result: Arc::clone(&checkpoint_git_result), @@ -158,11 +154,12 @@ impl WorkflowLifecycle { let artifact = ArtifactLifecycle::new( Arc::clone(sandbox), - Arc::clone(&artifact_store), - Some(runtime_state.artifact_values_dir()), + run_store.clone(), + runtime_state.blob_cache_dir(), Arc::clone(emitter), - runtime_state.assets_dir(), - run_options.asset_globs().to_vec(), + runtime_state.artifacts_dir(), + run_options.artifact_globs().to_vec(), + captured_artifact_count, ); Self { diff --git a/lib/crates/fabro-workflow/src/run_dump.rs b/lib/crates/fabro-workflow/src/run_dump.rs index c3dee22d7..154b2f52c 100644 --- a/lib/crates/fabro-workflow/src/run_dump.rs +++ b/lib/crates/fabro-workflow/src/run_dump.rs @@ -218,11 +218,11 @@ impl RunDump { )); } - for asset in run_store.list_all_assets().await? { + for asset in run_store.list_all_artifacts().await? { let node_id_segment = validate_single_path_segment("node id", asset.node.node_id())?; - let filename_path = validate_relative_path("asset filename", &asset.filename)?; + let filename_path = validate_relative_path("artifact filename", &asset.filename)?; let data = run_store - .get_asset(&asset.node, &asset.filename) + .get_artifact(&asset.node, &asset.filename) .await? .with_context(|| { format!( diff --git a/lib/crates/fabro-workflow/src/run_options.rs b/lib/crates/fabro-workflow/src/run_options.rs index 61ea9fb44..a01da6471 100644 --- a/lib/crates/fabro-workflow/src/run_options.rs +++ b/lib/crates/fabro-workflow/src/run_options.rs @@ -58,9 +58,9 @@ impl RunOptions { self.settings.pull_request.as_ref() } - pub fn asset_globs(&self) -> &[String] { + pub fn artifact_globs(&self) -> &[String] { self.settings - .assets + .artifacts .as_ref() .map_or(&[], |a| a.include.as_slice()) } diff --git a/lib/crates/fabro-workflow/src/sandbox_git.rs b/lib/crates/fabro-workflow/src/sandbox_git.rs index a2de0ed85..b58827a8d 100644 --- a/lib/crates/fabro-workflow/src/sandbox_git.rs +++ b/lib/crates/fabro-workflow/src/sandbox_git.rs @@ -5,7 +5,7 @@ use fabro_checkpoint::trailer as trailerlink; use fabro_checkpoint::trailer::Trailer; use fabro_types::RunId; -use crate::asset_snapshot; +use crate::artifact_snapshot; use crate::git::{GitAuthor, blocking_push_with_timeout, push_ref}; use fabro_sandbox::daytona::detect_repo_info; @@ -42,7 +42,7 @@ pub async fn git_checkpoint( exclude_globs: &[String], author: &GitAuthor, ) -> std::result::Result { - let mut all_excludes: Vec = asset_snapshot::EXCLUDE_DIRS + let mut all_excludes: Vec = artifact_snapshot::EXCLUDE_DIRS .iter() .map(|d| format!("**/{d}/**")) .collect(); diff --git a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs index 5fed2d954..ef1a65ef8 100644 --- a/lib/crates/fabro-workflow/tests/it/daytona_integration.rs +++ b/lib/crates/fabro-workflow/tests/it/daytona_integration.rs @@ -1243,10 +1243,10 @@ async fn daytona_git_checkpoint_with_shadow_branch() { } // --------------------------------------------------------------------------- -// Asset collection e2e — Daytona sandbox +// Artifact collection e2e — Daytona sandbox // --------------------------------------------------------------------------- -/// Handler that creates asset files via exec_command on the sandbox. +/// Handler that creates artifact files via exec_command on the sandbox. struct AssetCreatorHandler; #[async_trait::async_trait] @@ -1273,7 +1273,7 @@ impl Handler for AssetCreatorHandler { } } -/// Daytona sandbox: asset collection discovers files on the remote sandbox and +/// Daytona sandbox: artifact collection discovers files on the remote sandbox and /// downloads them to the local logs directory. #[fabro_macros::e2e_test(live("DAYTONA_API_KEY"), live("GITHUB_APP_PRIVATE_KEY"))] async fn daytona_asset_collection() { @@ -1292,7 +1292,7 @@ async fn daytona_asset_collection() { let mut graph = Graph::new("DaytonaAssetTest"); graph.attrs.insert( "goal".to_string(), - AttrValue::String("Test asset collection on Daytona".to_string()), + AttrValue::String("Test artifact collection on Daytona".to_string()), ); let mut start = Node::new("start"); @@ -1323,14 +1323,14 @@ async fn daytona_asset_collection() { let run_options = RunOptions { settings: Settings { - assets: Some(fabro_config::run::AssetsSettings { + artifacts: Some(fabro_config::run::ArtifactsSettings { include: vec!["test-results/**".to_string()], }), ..Settings::default() }, run_dir: dir.path().to_path_buf(), cancel_token: None, - run_id: test_run_id("asset-test-daytona"), + run_id: test_run_id("artifact-test-daytona"), labels: std::collections::HashMap::new(), workflow_slug: None, github_app: None, @@ -1345,9 +1345,9 @@ async fn daytona_asset_collection() { .expect("pipeline should succeed"); assert_eq!(outcome.status, StageStatus::Success); - let assets_dir = RuntimeState::new(dir.path()).asset_stage_dir("create_assets", 1); + let artifacts_dir = RuntimeState::new(dir.path()).artifact_stage_dir("create_assets", 1); - let report_path = assets_dir.join("test-results/report.xml"); + let report_path = artifacts_dir.join("test-results/report.xml"); assert!( report_path.exists(), "report.xml should be collected from Daytona sandbox at {}", @@ -1356,7 +1356,7 @@ async fn daytona_asset_collection() { let content = std::fs::read_to_string(&report_path).unwrap(); assert!(content.contains("testsuites")); - let manifest_path = assets_dir.join("manifest.json"); + let manifest_path = artifacts_dir.join("manifest.json"); assert!(manifest_path.exists(), "manifest.json should exist"); env.cleanup().await.unwrap(); diff --git a/lib/crates/fabro-workflow/tests/it/integration.rs b/lib/crates/fabro-workflow/tests/it/integration.rs index feb0c7c3f..7d7e33917 100644 --- a/lib/crates/fabro-workflow/tests/it/integration.rs +++ b/lib/crates/fabro-workflow/tests/it/integration.rs @@ -8569,8 +8569,16 @@ async fn large_context_values_are_offloaded_to_artifact_store() { "value should be an artifact pointer, got: {pointer_str}" ); + let expected_blob_id = fabro_types::RunBlobId::new( + &run_options.run_id, + &serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024))) + .expect("large value should serialize"), + ); + // The artifact file should exist on disk - let artifact_file = RuntimeState::new(dir.path()).artifact_value_path("response.big_output"); + let artifact_file = RuntimeState::new(dir.path()) + .artifact_values_dir() + .join(format!("{expected_blob_id}.json")); assert!( artifact_file.exists(), "artifact file should exist at {artifact_file:?}" @@ -8588,7 +8596,7 @@ async fn large_context_values_are_offloaded_to_artifact_store() { "artifact should contain the original 150KB value" ); - // WorkflowRunCompleted event should report artifact_count > 0 + // WorkflowRunCompleted artifact_count now tracks captured artifacts, not offloaded values. let evts = events.lock().unwrap(); let completed_event = evts .iter() @@ -8597,9 +8605,9 @@ async fn large_context_values_are_offloaded_to_artifact_store() { let artifact_count = completed_event.properties["artifact_count"] .as_u64() .expect("run.completed should include artifact_count"); - assert!( - artifact_count > 0, - "artifact_count should be > 0, got {artifact_count}" + assert_eq!( + artifact_count, 0, + "artifact_count should ignore offloaded values" ); } @@ -12294,10 +12302,10 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() { // Daytona parallel git branching test is in daytona_integration.rs // --------------------------------------------------------------------------- -// Asset collection e2e tests +// Artifact collection e2e tests // --------------------------------------------------------------------------- -/// Handler that creates asset files in the sandbox working directory via exec_command. +/// Handler that creates artifact files in the sandbox working directory via exec_command. struct AssetCreatorHandler { should_fail: bool, } @@ -12322,7 +12330,7 @@ impl Handler for AssetCreatorHandler { _run_dir: &Path, services: &fabro_workflow::handler::EngineServices, ) -> Result { - // Create asset files via the sandbox's exec_command + // Create artifact files via the sandbox's exec_command let script = concat!( "mkdir -p test-results && ", "echo '' > test-results/report.xml && ", @@ -12342,7 +12350,7 @@ impl Handler for AssetCreatorHandler { } } -/// Local sandbox: asset collection discovers and downloads files created by a handler. +/// Local sandbox: artifact collection discovers and downloads files created by a handler. #[tokio::test] async fn asset_collection_local_sandbox_success() { let work_dir = tempfile::tempdir().unwrap(); @@ -12365,7 +12373,7 @@ async fn asset_collection_local_sandbox_success() { let mut graph = Graph::new("AssetCollectionTest"); graph.attrs.insert( "goal".to_string(), - AttrValue::String("Test asset collection".to_string()), + AttrValue::String("Test artifact collection".to_string()), ); let mut start = Node::new("start"); @@ -12396,14 +12404,14 @@ async fn asset_collection_local_sandbox_success() { let run_options = RunOptions { settings: Settings { - assets: Some(fabro_config::run::AssetsSettings { + artifacts: Some(fabro_config::run::ArtifactsSettings { include: vec!["test-results/**".to_string()], }), ..Settings::default() }, run_dir: run_dir.path().to_path_buf(), cancel_token: None, - run_id: test_run_id("asset-test-local"), + run_id: test_run_id("artifact-test-local"), labels: std::collections::HashMap::new(), workflow_slug: None, github_app: None, @@ -12418,10 +12426,10 @@ async fn asset_collection_local_sandbox_success() { .expect("run should succeed"); assert_eq!(outcome.status, StageStatus::Success); - // Check that asset files were collected into the stage directory - let assets_dir = RuntimeState::new(run_dir.path()).asset_stage_dir("create_assets", 1); + // Check that artifact files were collected into the stage directory + let artifacts_dir = RuntimeState::new(run_dir.path()).artifact_stage_dir("create_assets", 1); - let report_path = assets_dir.join("test-results/report.xml"); + let report_path = artifacts_dir.join("test-results/report.xml"); assert!( report_path.exists(), "report.xml should be collected at {}", @@ -12431,7 +12439,7 @@ async fn asset_collection_local_sandbox_success() { assert!(report_content.contains("testsuites")); // Check manifest.json was written - let manifest_path = assets_dir.join("manifest.json"); + let manifest_path = artifacts_dir.join("manifest.json"); assert!( manifest_path.exists(), "manifest.json should exist at {}", @@ -12441,15 +12449,15 @@ async fn asset_collection_local_sandbox_success() { serde_json::from_str(&std::fs::read_to_string(&manifest_path).unwrap()).unwrap(); assert!(manifest["files_copied"].as_u64().unwrap() >= 1); - // Check that AssetCaptured events were emitted + // Check that ArtifactCaptured events were emitted let captured_events = events.lock().unwrap(); let asset_events: Vec<&RunEvent> = captured_events .iter() - .filter(|e| e.event == "asset.captured") + .filter(|e| e.event == "artifact.captured") .collect(); assert!( !asset_events.is_empty(), - "should emit at least one AssetCaptured event" + "should emit at least one ArtifactCaptured event" ); let asset_event = asset_events[0]; assert!(!asset_event.properties["path"].as_str().unwrap().is_empty()); @@ -12492,7 +12500,7 @@ async fn asset_collection_local_sandbox_on_failure() { let mut graph = Graph::new("AssetCollectionFailTest"); graph.attrs.insert( "goal".to_string(), - AttrValue::String("Test asset collection on failure".to_string()), + AttrValue::String("Test artifact collection on failure".to_string()), ); let mut start = Node::new("start"); @@ -12523,14 +12531,14 @@ async fn asset_collection_local_sandbox_on_failure() { let run_options = RunOptions { settings: Settings { - assets: Some(fabro_config::run::AssetsSettings { + artifacts: Some(fabro_config::run::ArtifactsSettings { include: vec!["test-results/**".to_string()], }), ..Settings::default() }, run_dir: run_dir.path().to_path_buf(), cancel_token: None, - run_id: test_run_id("asset-test-fail"), + run_id: test_run_id("artifact-test-fail"), labels: std::collections::HashMap::new(), workflow_slug: None, github_app: None, @@ -12547,9 +12555,9 @@ async fn asset_collection_local_sandbox_on_failure() { // Assets should still be collected regardless of intermediate node failures. assert_eq!(outcome.status, StageStatus::Success); - let assets_dir = RuntimeState::new(run_dir.path()).asset_stage_dir("create_assets", 1); + let artifacts_dir = RuntimeState::new(run_dir.path()).artifact_stage_dir("create_assets", 1); - let report_path = assets_dir.join("test-results/report.xml"); + let report_path = artifacts_dir.join("test-results/report.xml"); assert!( report_path.exists(), "report.xml should still be collected after handler failure, at {}", @@ -12557,7 +12565,7 @@ async fn asset_collection_local_sandbox_on_failure() { ); } -/// Docker sandbox: asset collection works across the bind-mount boundary. +/// Docker sandbox: artifact collection works across the bind-mount boundary. /// Requires Docker with `fabro-agent:latest` image available locally. #[tokio::test] #[ignore] @@ -12583,7 +12591,7 @@ async fn asset_collection_docker_sandbox() { let mut graph = Graph::new("DockerAssetTest"); graph.attrs.insert( "goal".to_string(), - AttrValue::String("Test asset collection in Docker".to_string()), + AttrValue::String("Test artifact collection in Docker".to_string()), ); let mut start = Node::new("start"); @@ -12614,14 +12622,14 @@ async fn asset_collection_docker_sandbox() { let run_options = RunOptions { settings: Settings { - assets: Some(fabro_config::run::AssetsSettings { + artifacts: Some(fabro_config::run::ArtifactsSettings { include: vec!["test-results/**".to_string()], }), ..Settings::default() }, run_dir: run_dir.path().to_path_buf(), cancel_token: None, - run_id: test_run_id("asset-test-docker"), + run_id: test_run_id("artifact-test-docker"), labels: std::collections::HashMap::new(), workflow_slug: None, github_app: None, @@ -12636,9 +12644,9 @@ async fn asset_collection_docker_sandbox() { .expect("pipeline should succeed"); assert_eq!(outcome.status, StageStatus::Success); - let assets_dir = RuntimeState::new(run_dir.path()).asset_stage_dir("create_assets", 1); + let artifacts_dir = RuntimeState::new(run_dir.path()).artifact_stage_dir("create_assets", 1); - let report_path = assets_dir.join("test-results/report.xml"); + let report_path = artifacts_dir.join("test-results/report.xml"); assert!( report_path.exists(), "report.xml should be collected from Docker container at {}", @@ -12647,7 +12655,7 @@ async fn asset_collection_docker_sandbox() { let content = std::fs::read_to_string(&report_path).unwrap(); assert!(content.contains("testsuites")); - let manifest_path = assets_dir.join("manifest.json"); + let manifest_path = artifacts_dir.join("manifest.json"); assert!(manifest_path.exists(), "manifest.json should exist"); sandbox.cleanup().await.unwrap(); diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 7a3ebe768..ee229ad3b 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -22,7 +22,7 @@ models/aggregate-usage.ts models/api-question-option.ts models/api-question.ts models/api-settings.ts -models/assets-settings.ts +models/artifacts-settings.ts models/assistant-stage-turn.ts models/assistant-turn.ts models/auth-settings.ts @@ -54,7 +54,6 @@ models/diff-file.ts models/diff-stats.ts models/error-response-entry.ts models/error-response.ts -models/exe-settings.ts models/execute-query-request.ts models/execute-query-response-rows-inner-inner.ts models/execute-query-response.ts @@ -141,7 +140,6 @@ models/sibling-control.ts models/signoff-status.ts models/signoff.ts models/smoothness-rating.ts -models/ssh-settings.ts models/stage-retro.ts models/stage-status.ts models/stage-turn.ts diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/VERSION b/lib/packages/fabro-api-client/src/.openapi-generator/VERSION index a29ba3d5c..2540a3a5b 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/VERSION +++ b/lib/packages/fabro-api-client/src/.openapi-generator/VERSION @@ -1 +1 @@ -7.21.0 +7.20.0 diff --git a/lib/packages/fabro-api-client/src/models/api-settings.ts b/lib/packages/fabro-api-client/src/models/api-settings.ts index 68e9c6159..ed5b34002 100644 --- a/lib/packages/fabro-api-client/src/models/api-settings.ts +++ b/lib/packages/fabro-api-client/src/models/api-settings.ts @@ -34,7 +34,7 @@ export interface ApiSettings { export const ApiSettingsAuthenticationStrategiesEnum = { JWT: 'jwt', - MTLS: 'mtls', + MTLS: 'mtls' } as const; export type ApiSettingsAuthenticationStrategiesEnum = typeof ApiSettingsAuthenticationStrategiesEnum[keyof typeof ApiSettingsAuthenticationStrategiesEnum]; diff --git a/lib/packages/fabro-api-client/src/models/assets-configuration.ts b/lib/packages/fabro-api-client/src/models/artifacts-configuration.ts similarity index 74% rename from lib/packages/fabro-api-client/src/models/assets-configuration.ts rename to lib/packages/fabro-api-client/src/models/artifacts-configuration.ts index 37a2b3d37..a1acd2ae9 100644 --- a/lib/packages/fabro-api-client/src/models/assets-configuration.ts +++ b/lib/packages/fabro-api-client/src/models/artifacts-configuration.ts @@ -15,11 +15,11 @@ /** - * Asset collection configuration. + * Artifact collection configuration. */ -export interface AssetsSettings { +export interface ArtifactsSettings { /** - * Glob patterns for files to collect as run assets. + * Glob patterns for files to collect as run artifacts. */ 'include'?: Array; } diff --git a/lib/packages/fabro-api-client/src/models/assets-settings.ts b/lib/packages/fabro-api-client/src/models/artifacts-settings.ts similarity index 74% rename from lib/packages/fabro-api-client/src/models/assets-settings.ts rename to lib/packages/fabro-api-client/src/models/artifacts-settings.ts index 37a2b3d37..a1acd2ae9 100644 --- a/lib/packages/fabro-api-client/src/models/assets-settings.ts +++ b/lib/packages/fabro-api-client/src/models/artifacts-settings.ts @@ -15,11 +15,11 @@ /** - * Asset collection configuration. + * Artifact collection configuration. */ -export interface AssetsSettings { +export interface ArtifactsSettings { /** - * Glob patterns for files to collect as run assets. + * Glob patterns for files to collect as run artifacts. */ 'include'?: Array; } diff --git a/lib/packages/fabro-api-client/src/models/assistant-stage-turn.ts b/lib/packages/fabro-api-client/src/models/assistant-stage-turn.ts index 6e323ccb0..8e0a8edcf 100644 --- a/lib/packages/fabro-api-client/src/models/assistant-stage-turn.ts +++ b/lib/packages/fabro-api-client/src/models/assistant-stage-turn.ts @@ -26,7 +26,7 @@ export interface AssistantStageTurn { } export const AssistantStageTurnKindEnum = { - ASSISTANT: 'assistant', + ASSISTANT: 'assistant' } as const; export type AssistantStageTurnKindEnum = typeof AssistantStageTurnKindEnum[keyof typeof AssistantStageTurnKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/assistant-turn.ts b/lib/packages/fabro-api-client/src/models/assistant-turn.ts index 14b89c9bb..646c3af37 100644 --- a/lib/packages/fabro-api-client/src/models/assistant-turn.ts +++ b/lib/packages/fabro-api-client/src/models/assistant-turn.ts @@ -30,7 +30,7 @@ export interface AssistantTurn { } export const AssistantTurnKindEnum = { - ASSISTANT: 'assistant', + ASSISTANT: 'assistant' } as const; export type AssistantTurnKindEnum = typeof AssistantTurnKindEnum[keyof typeof AssistantTurnKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/auth-settings.ts b/lib/packages/fabro-api-client/src/models/auth-settings.ts index 8951ecc78..d7763c463 100644 --- a/lib/packages/fabro-api-client/src/models/auth-settings.ts +++ b/lib/packages/fabro-api-client/src/models/auth-settings.ts @@ -30,7 +30,7 @@ export interface AuthSettings { export const AuthSettingsProviderEnum = { GITHUB: 'github', - INSECURE_DISABLED: 'insecure_disabled', + INSECURE_DISABLED: 'insecure_disabled' } as const; export type AuthSettingsProviderEnum = typeof AuthSettingsProviderEnum[keyof typeof AuthSettingsProviderEnum]; diff --git a/lib/packages/fabro-api-client/src/models/board-column.ts b/lib/packages/fabro-api-client/src/models/board-column.ts index 584655249..c40047634 100644 --- a/lib/packages/fabro-api-client/src/models/board-column.ts +++ b/lib/packages/fabro-api-client/src/models/board-column.ts @@ -22,7 +22,7 @@ export const BoardColumn = { WORKING: 'working', PENDING: 'pending', REVIEW: 'review', - MERGE: 'merge', + MERGE: 'merge' } as const; export type BoardColumn = typeof BoardColumn[keyof typeof BoardColumn]; diff --git a/lib/packages/fabro-api-client/src/models/check-run-status.ts b/lib/packages/fabro-api-client/src/models/check-run-status.ts index e5d6224cc..a1cc8fd72 100644 --- a/lib/packages/fabro-api-client/src/models/check-run-status.ts +++ b/lib/packages/fabro-api-client/src/models/check-run-status.ts @@ -23,7 +23,7 @@ export const CheckRunStatus = { FAILURE: 'failure', SKIPPED: 'skipped', PENDING: 'pending', - QUEUED: 'queued', + QUEUED: 'queued' } as const; export type CheckRunStatus = typeof CheckRunStatus[keyof typeof CheckRunStatus]; diff --git a/lib/packages/fabro-api-client/src/models/completion-message.ts b/lib/packages/fabro-api-client/src/models/completion-message.ts index 440d98bc7..75ca6b9ff 100644 --- a/lib/packages/fabro-api-client/src/models/completion-message.ts +++ b/lib/packages/fabro-api-client/src/models/completion-message.ts @@ -44,7 +44,7 @@ export const CompletionMessageRoleEnum = { USER: 'user', ASSISTANT: 'assistant', TOOL: 'tool', - DEVELOPER: 'developer', + DEVELOPER: 'developer' } as const; export type CompletionMessageRoleEnum = typeof CompletionMessageRoleEnum[keyof typeof CompletionMessageRoleEnum]; diff --git a/lib/packages/fabro-api-client/src/models/completion-tool-choice.ts b/lib/packages/fabro-api-client/src/models/completion-tool-choice.ts index 225a57218..3496175db 100644 --- a/lib/packages/fabro-api-client/src/models/completion-tool-choice.ts +++ b/lib/packages/fabro-api-client/src/models/completion-tool-choice.ts @@ -32,7 +32,7 @@ export const CompletionToolChoiceModeEnum = { AUTO: 'auto', NONE: 'none', REQUIRED: 'required', - NAMED: 'named', + NAMED: 'named' } as const; export type CompletionToolChoiceModeEnum = typeof CompletionToolChoiceModeEnum[keyof typeof CompletionToolChoiceModeEnum]; diff --git a/lib/packages/fabro-api-client/src/models/friction-kind.ts b/lib/packages/fabro-api-client/src/models/friction-kind.ts index d68094359..4a368e159 100644 --- a/lib/packages/fabro-api-client/src/models/friction-kind.ts +++ b/lib/packages/fabro-api-client/src/models/friction-kind.ts @@ -23,7 +23,7 @@ export const FrictionKind = { TIMEOUT: 'timeout', WRONG_APPROACH: 'wrong_approach', TOOL_FAILURE: 'tool_failure', - AMBIGUITY: 'ambiguity', + AMBIGUITY: 'ambiguity' } as const; export type FrictionKind = typeof FrictionKind[keyof typeof FrictionKind]; diff --git a/lib/packages/fabro-api-client/src/models/git-settings.ts b/lib/packages/fabro-api-client/src/models/git-settings.ts index a2534f41b..a37da07ba 100644 --- a/lib/packages/fabro-api-client/src/models/git-settings.ts +++ b/lib/packages/fabro-api-client/src/models/git-settings.ts @@ -45,7 +45,7 @@ export interface GitSettings { } export const GitSettingsProviderEnum = { - GITHUB: 'github', + GITHUB: 'github' } as const; export type GitSettingsProviderEnum = typeof GitSettingsProviderEnum[keyof typeof GitSettingsProviderEnum]; diff --git a/lib/packages/fabro-api-client/src/models/hook-definition.ts b/lib/packages/fabro-api-client/src/models/hook-definition.ts index 56c61c32d..b0717999b 100644 --- a/lib/packages/fabro-api-client/src/models/hook-definition.ts +++ b/lib/packages/fabro-api-client/src/models/hook-definition.ts @@ -84,7 +84,7 @@ export const HookDefinitionEventEnum = { RUN_START: 'run_start', RUN_COMPLETE: 'run_complete', STAGE_START: 'stage_start', - STAGE_COMPLETE: 'stage_complete', + STAGE_COMPLETE: 'stage_complete' } as const; export type HookDefinitionEventEnum = typeof HookDefinitionEventEnum[keyof typeof HookDefinitionEventEnum]; @@ -92,14 +92,14 @@ export const HookDefinitionTypeEnum = { COMMAND: 'command', HTTP: 'http', PROMPT: 'prompt', - AGENT: 'agent', + AGENT: 'agent' } as const; export type HookDefinitionTypeEnum = typeof HookDefinitionTypeEnum[keyof typeof HookDefinitionTypeEnum]; export const HookDefinitionTlsEnum = { VERIFY: 'verify', NO_VERIFY: 'no_verify', - OFF: 'off', + OFF: 'off' } as const; export type HookDefinitionTlsEnum = typeof HookDefinitionTlsEnum[keyof typeof HookDefinitionTlsEnum]; diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index fc4f12efb..62cc88b7b 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -3,7 +3,7 @@ export * from './aggregate-usage-totals'; export * from './api-question'; export * from './api-question-option'; export * from './api-settings'; -export * from './assets-settings'; +export * from './artifacts-settings'; export * from './assistant-stage-turn'; export * from './assistant-turn'; export * from './auth-settings'; @@ -35,7 +35,6 @@ export * from './diff-file'; export * from './diff-stats'; export * from './error-response'; export * from './error-response-entry'; -export * from './exe-settings'; export * from './execute-query-request'; export * from './execute-query-response'; export * from './execute-query-response-rows-inner-inner'; @@ -121,7 +120,6 @@ export * from './sibling-control'; export * from './signoff'; export * from './signoff-status'; export * from './smoothness-rating'; -export * from './ssh-settings'; export * from './stage-retro'; export * from './stage-status'; export * from './stage-turn'; diff --git a/lib/packages/fabro-api-client/src/models/learning-category.ts b/lib/packages/fabro-api-client/src/models/learning-category.ts index 25ab790fa..c5bee9fbb 100644 --- a/lib/packages/fabro-api-client/src/models/learning-category.ts +++ b/lib/packages/fabro-api-client/src/models/learning-category.ts @@ -22,7 +22,7 @@ export const LearningCategory = { REPO: 'repo', CODE: 'code', WORKFLOW: 'workflow', - TOOL: 'tool', + TOOL: 'tool' } as const; export type LearningCategory = typeof LearningCategory[keyof typeof LearningCategory]; diff --git a/lib/packages/fabro-api-client/src/models/local-sandbox-settings.ts b/lib/packages/fabro-api-client/src/models/local-sandbox-settings.ts index 8055e7ebd..dca9c61d4 100644 --- a/lib/packages/fabro-api-client/src/models/local-sandbox-settings.ts +++ b/lib/packages/fabro-api-client/src/models/local-sandbox-settings.ts @@ -28,7 +28,7 @@ export const LocalSandboxSettingsWorktreeModeEnum = { ALWAYS: 'always', CLEAN: 'clean', DIRTY: 'dirty', - NEVER: 'never', + NEVER: 'never' } as const; export type LocalSandboxSettingsWorktreeModeEnum = typeof LocalSandboxSettingsWorktreeModeEnum[keyof typeof LocalSandboxSettingsWorktreeModeEnum]; diff --git a/lib/packages/fabro-api-client/src/models/model-test-result.ts b/lib/packages/fabro-api-client/src/models/model-test-result.ts index 7776c6b65..844d0c3db 100644 --- a/lib/packages/fabro-api-client/src/models/model-test-result.ts +++ b/lib/packages/fabro-api-client/src/models/model-test-result.ts @@ -34,7 +34,7 @@ export interface ModelTestResult { export const ModelTestResultStatusEnum = { OK: 'ok', - ERROR: 'error', + ERROR: 'error' } as const; export type ModelTestResultStatusEnum = typeof ModelTestResultStatusEnum[keyof typeof ModelTestResultStatusEnum]; diff --git a/lib/packages/fabro-api-client/src/models/open-item-kind.ts b/lib/packages/fabro-api-client/src/models/open-item-kind.ts index 68b0780f1..4a77e2ab9 100644 --- a/lib/packages/fabro-api-client/src/models/open-item-kind.ts +++ b/lib/packages/fabro-api-client/src/models/open-item-kind.ts @@ -22,7 +22,7 @@ export const OpenItemKind = { TECH_DEBT: 'tech_debt', FOLLOW_UP: 'follow_up', INVESTIGATION: 'investigation', - TEST_GAP: 'test_gap', + TEST_GAP: 'test_gap' } as const; export type OpenItemKind = typeof OpenItemKind[keyof typeof OpenItemKind]; diff --git a/lib/packages/fabro-api-client/src/models/pull-request-settings.ts b/lib/packages/fabro-api-client/src/models/pull-request-settings.ts index c2af9b9c5..548fa471b 100644 --- a/lib/packages/fabro-api-client/src/models/pull-request-settings.ts +++ b/lib/packages/fabro-api-client/src/models/pull-request-settings.ts @@ -39,7 +39,7 @@ export interface PullRequestSettings { export const PullRequestSettingsMergeStrategyEnum = { SQUASH: 'squash', MERGE: 'merge', - REBASE: 'rebase', + REBASE: 'rebase' } as const; export type PullRequestSettingsMergeStrategyEnum = typeof PullRequestSettingsMergeStrategyEnum[keyof typeof PullRequestSettingsMergeStrategyEnum]; diff --git a/lib/packages/fabro-api-client/src/models/question-type.ts b/lib/packages/fabro-api-client/src/models/question-type.ts index 294acd398..fce56e731 100644 --- a/lib/packages/fabro-api-client/src/models/question-type.ts +++ b/lib/packages/fabro-api-client/src/models/question-type.ts @@ -23,7 +23,7 @@ export const QuestionType = { MULTIPLE_CHOICE: 'multiple_choice', MULTI_SELECT: 'multi_select', FREEFORM: 'freeform', - CONFIRMATION: 'confirmation', + CONFIRMATION: 'confirmation' } as const; export type QuestionType = typeof QuestionType[keyof typeof QuestionType]; diff --git a/lib/packages/fabro-api-client/src/models/run-settings.ts b/lib/packages/fabro-api-client/src/models/run-settings.ts index 6a8b4309d..37e72d7a2 100644 --- a/lib/packages/fabro-api-client/src/models/run-settings.ts +++ b/lib/packages/fabro-api-client/src/models/run-settings.ts @@ -27,7 +27,7 @@ import type { SandboxSettings } from './sandbox-settings'; import type { SetupSettings } from './setup-settings'; /** - * Structured run settings mirroring FabroSettings. + * Structured run settings mirroring fabro_types::Settings. */ export interface RunSettings { /** diff --git a/lib/packages/fabro-api-client/src/models/run-status.ts b/lib/packages/fabro-api-client/src/models/run-status.ts index dd5b4a4d1..cc748cf77 100644 --- a/lib/packages/fabro-api-client/src/models/run-status.ts +++ b/lib/packages/fabro-api-client/src/models/run-status.ts @@ -25,7 +25,7 @@ export const RunStatus = { COMPLETED: 'completed', FAILED: 'failed', CANCELLED: 'cancelled', - PAUSED: 'paused', + PAUSED: 'paused' } as const; export type RunStatus = typeof RunStatus[keyof typeof RunStatus]; diff --git a/lib/packages/fabro-api-client/src/models/sandbox-settings.ts b/lib/packages/fabro-api-client/src/models/sandbox-settings.ts index 9f439a793..ed50c1853 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-settings.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-settings.ts @@ -18,13 +18,7 @@ import type { DaytonaSettings } from './daytona-settings'; // May contain unused imports in some cases // @ts-ignore -import type { ExeSettings } from './exe-settings'; -// May contain unused imports in some cases -// @ts-ignore import type { LocalSandboxSettings } from './local-sandbox-settings'; -// May contain unused imports in some cases -// @ts-ignore -import type { SshSettings } from './ssh-settings'; /** * Sandbox execution environment settings. @@ -43,8 +37,6 @@ export interface SandboxSettings { */ 'devcontainer'?: boolean; 'daytona'?: DaytonaSettings; - 'exe'?: ExeSettings; - 'ssh'?: SshSettings; 'local'?: LocalSandboxSettings; /** * Environment variables injected into the sandbox. diff --git a/lib/packages/fabro-api-client/src/models/server-configuration.ts b/lib/packages/fabro-api-client/src/models/server-configuration.ts index bca30904b..b574c9d81 100644 --- a/lib/packages/fabro-api-client/src/models/server-configuration.ts +++ b/lib/packages/fabro-api-client/src/models/server-configuration.ts @@ -18,7 +18,7 @@ import type { ApiSettings } from './api-configuration'; // May contain unused imports in some cases // @ts-ignore -import type { AssetsSettings } from './assets-configuration'; +import type { ArtifactsSettings } from './artifacts-configuration'; // May contain unused imports in some cases // @ts-ignore import type { CheckpointSettings } from './checkpoint-configuration'; @@ -87,7 +87,7 @@ export interface ServerSettings { 'checkpoint'?: CheckpointSettings; 'pull_request'?: PullRequestSettings; 'hooks'?: Array; - 'assets'?: AssetsSettings; + 'artifacts'?: ArtifactsSettings; /** * Default MCP server configurations. */ diff --git a/lib/packages/fabro-api-client/src/models/server-settings.ts b/lib/packages/fabro-api-client/src/models/server-settings.ts index 869e5886d..dbbd55247 100644 --- a/lib/packages/fabro-api-client/src/models/server-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-settings.ts @@ -18,7 +18,7 @@ import type { ApiSettings } from './api-settings'; // May contain unused imports in some cases // @ts-ignore -import type { AssetsSettings } from './assets-settings'; +import type { ArtifactsSettings } from './artifacts-settings'; // May contain unused imports in some cases // @ts-ignore import type { CheckpointSettings } from './checkpoint-settings'; @@ -57,7 +57,7 @@ import type { SetupSettings } from './setup-settings'; import type { WebSettings } from './web-settings'; /** - * Structured server settings mirroring FabroSettings. + * Structured server settings mirroring fabro_types::Settings. */ export interface ServerSettings { /** @@ -87,7 +87,7 @@ export interface ServerSettings { 'checkpoint'?: CheckpointSettings; 'pull_request'?: PullRequestSettings; 'hooks'?: Array; - 'assets'?: AssetsSettings; + 'artifacts'?: ArtifactsSettings; /** * Default MCP server configurations. */ diff --git a/lib/packages/fabro-api-client/src/models/signoff-status.ts b/lib/packages/fabro-api-client/src/models/signoff-status.ts index 7f52cd401..84a7f7149 100644 --- a/lib/packages/fabro-api-client/src/models/signoff-status.ts +++ b/lib/packages/fabro-api-client/src/models/signoff-status.ts @@ -21,7 +21,7 @@ export const SignoffStatus = { PASS: 'pass', FAIL: 'fail', - PENDING: 'pending', + PENDING: 'pending' } as const; export type SignoffStatus = typeof SignoffStatus[keyof typeof SignoffStatus]; diff --git a/lib/packages/fabro-api-client/src/models/smoothness-rating.ts b/lib/packages/fabro-api-client/src/models/smoothness-rating.ts index ba37c50bb..a11203368 100644 --- a/lib/packages/fabro-api-client/src/models/smoothness-rating.ts +++ b/lib/packages/fabro-api-client/src/models/smoothness-rating.ts @@ -23,7 +23,7 @@ export const SmoothnessRating = { SMOOTH: 'smooth', BUMPY: 'bumpy', STRUGGLED: 'struggled', - FAILED: 'failed', + FAILED: 'failed' } as const; export type SmoothnessRating = typeof SmoothnessRating[keyof typeof SmoothnessRating]; diff --git a/lib/packages/fabro-api-client/src/models/stage-status.ts b/lib/packages/fabro-api-client/src/models/stage-status.ts index 77d8a0878..0ef7faa41 100644 --- a/lib/packages/fabro-api-client/src/models/stage-status.ts +++ b/lib/packages/fabro-api-client/src/models/stage-status.ts @@ -23,7 +23,7 @@ export const StageStatus = { RUNNING: 'running', PENDING: 'pending', FAILED: 'failed', - CANCELLED: 'cancelled', + CANCELLED: 'cancelled' } as const; export type StageStatus = typeof StageStatus[keyof typeof StageStatus]; diff --git a/lib/packages/fabro-api-client/src/models/system-stage-turn.ts b/lib/packages/fabro-api-client/src/models/system-stage-turn.ts index 0310c2a90..68ad6386f 100644 --- a/lib/packages/fabro-api-client/src/models/system-stage-turn.ts +++ b/lib/packages/fabro-api-client/src/models/system-stage-turn.ts @@ -26,7 +26,7 @@ export interface SystemStageTurn { } export const SystemStageTurnKindEnum = { - SYSTEM: 'system', + SYSTEM: 'system' } as const; export type SystemStageTurnKindEnum = typeof SystemStageTurnKindEnum[keyof typeof SystemStageTurnKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/tool-stage-turn.ts b/lib/packages/fabro-api-client/src/models/tool-stage-turn.ts index dc1f0ebf8..e637a0520 100644 --- a/lib/packages/fabro-api-client/src/models/tool-stage-turn.ts +++ b/lib/packages/fabro-api-client/src/models/tool-stage-turn.ts @@ -33,7 +33,7 @@ export interface ToolStageTurn { } export const ToolStageTurnKindEnum = { - TOOL: 'tool', + TOOL: 'tool' } as const; export type ToolStageTurnKindEnum = typeof ToolStageTurnKindEnum[keyof typeof ToolStageTurnKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/tool-turn.ts b/lib/packages/fabro-api-client/src/models/tool-turn.ts index eac69ad08..9a1ddfb71 100644 --- a/lib/packages/fabro-api-client/src/models/tool-turn.ts +++ b/lib/packages/fabro-api-client/src/models/tool-turn.ts @@ -33,7 +33,7 @@ export interface ToolTurn { } export const ToolTurnKindEnum = { - TOOL: 'tool', + TOOL: 'tool' } as const; export type ToolTurnKindEnum = typeof ToolTurnKindEnum[keyof typeof ToolTurnKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/user-turn.ts b/lib/packages/fabro-api-client/src/models/user-turn.ts index 73cdc6cbf..2e8be3bfd 100644 --- a/lib/packages/fabro-api-client/src/models/user-turn.ts +++ b/lib/packages/fabro-api-client/src/models/user-turn.ts @@ -30,7 +30,7 @@ export interface UserTurn { } export const UserTurnKindEnum = { - USER: 'user', + USER: 'user' } as const; export type UserTurnKindEnum = typeof UserTurnKindEnum[keyof typeof UserTurnKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/verification-mode.ts b/lib/packages/fabro-api-client/src/models/verification-mode.ts index 5d1082d8f..cd7b5d745 100644 --- a/lib/packages/fabro-api-client/src/models/verification-mode.ts +++ b/lib/packages/fabro-api-client/src/models/verification-mode.ts @@ -21,7 +21,7 @@ export const VerificationMode = { ACTIVE: 'active', EVALUATE: 'evaluate', - DISABLED: 'disabled', + DISABLED: 'disabled' } as const; export type VerificationMode = typeof VerificationMode[keyof typeof VerificationMode]; diff --git a/lib/packages/fabro-api-client/src/models/verification-result.ts b/lib/packages/fabro-api-client/src/models/verification-result.ts index 333a7ba43..15d2641d3 100644 --- a/lib/packages/fabro-api-client/src/models/verification-result.ts +++ b/lib/packages/fabro-api-client/src/models/verification-result.ts @@ -22,7 +22,7 @@ export const VerificationResult = { PASS: 'pass', FAIL: 'fail', SKIP: 'skip', - NA: 'na', + NA: 'na' } as const; export type VerificationResult = typeof VerificationResult[keyof typeof VerificationResult]; diff --git a/lib/packages/fabro-api-client/src/models/verification-type.ts b/lib/packages/fabro-api-client/src/models/verification-type.ts index c0da24e4d..3f86896a8 100644 --- a/lib/packages/fabro-api-client/src/models/verification-type.ts +++ b/lib/packages/fabro-api-client/src/models/verification-type.ts @@ -22,7 +22,7 @@ export const VerificationType = { AI: 'ai', AUTOMATED: 'automated', ANALYSIS: 'analysis', - AI_ANALYSIS: 'ai-analysis', + AI_ANALYSIS: 'ai-analysis' } as const; export type VerificationType = typeof VerificationType[keyof typeof VerificationType]; diff --git a/lib/packages/fabro-api-client/src/models/webhook-settings.ts b/lib/packages/fabro-api-client/src/models/webhook-settings.ts index 1ea36ec8e..8bd2e9e39 100644 --- a/lib/packages/fabro-api-client/src/models/webhook-settings.ts +++ b/lib/packages/fabro-api-client/src/models/webhook-settings.ts @@ -25,7 +25,7 @@ export interface WebhookSettings { } export const WebhookSettingsStrategyEnum = { - TAILSCALE_FUNNEL: 'tailscale_funnel', + TAILSCALE_FUNNEL: 'tailscale_funnel' } as const; export type WebhookSettingsStrategyEnum = typeof WebhookSettingsStrategyEnum[keyof typeof WebhookSettingsStrategyEnum];