Make asset collection opt-in via [assets] config globs

Asset collection previously ran a full `find` scan before and after every
stage (~30s per scan), even when no assets were needed. This makes it
opt-in: only workflows with an `[assets] include = [...]` section in their
TOML config will run asset collection, and only with user-specified globs.

- Add AssetsConfig struct and wire through WorkflowRunConfig, RunDefaults,
  and RunConfig
- Remove baseline snapshot approach; single post-execution scan using
  user globs filtered by mtime and size budgets
- Delete hardcoded pattern constants, is_asset_candidate, matches_simple_glob,
  FileFingerprint, and snapshot() — replaced by configurable globs
- Skip all asset work when asset_globs is empty (zero overhead default)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-08 23:39:04 -04:00
parent 686d6bbdb0
commit 7400a9d5a1
9 changed files with 334 additions and 276 deletions

View file

@ -1293,6 +1293,7 @@ mod runs {
hooks: vec![],
checkpoint: Default::default(),
pull_request: None,
assets: None,
})
.unwrap()
}
@ -1443,6 +1444,7 @@ mod workflows {
hooks: vec![],
checkpoint: Default::default(),
pull_request: None,
assets: None,
}),
graph: r#"digraph fix_build {
graph [
@ -1510,6 +1512,7 @@ mod workflows {
hooks: vec![],
checkpoint: Default::default(),
pull_request: None,
assets: None,
}),
graph: r#"digraph implement {
graph [
@ -1589,6 +1592,7 @@ mod workflows {
hooks: vec![],
checkpoint: Default::default(),
pull_request: None,
assets: None,
}),
graph: r#"digraph sync {
graph [
@ -1657,6 +1661,7 @@ mod workflows {
hooks: vec![],
checkpoint: Default::default(),
pull_request: None,
assets: None,
}),
graph: r#"digraph expand {
graph [
@ -3097,6 +3102,7 @@ mod settings {
vars: None,
checkpoint: Default::default(),
pull_request: None,
assets: None,
},
hook_config: arc_workflows::hook::HookConfig { hooks: vec![] },
})

View file

@ -608,6 +608,7 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
git_author: state.git_author.clone(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = tokio::select! {

View file

@ -1,16 +1,8 @@
use arc_agent::Sandbox;
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
use tracing::{debug, warn};
/// Fingerprint of a file for change detection between snapshots.
#[derive(Debug, Clone, PartialEq)]
pub struct FileFingerprint {
pub size: u64,
pub mtime_epoch_secs: f64,
}
/// A file discovered by the find command.
#[derive(Debug, Clone)]
pub struct DiscoveredFile {
@ -29,17 +21,6 @@ pub struct AssetCollectionSummary {
pub copied_paths: Vec<String>,
}
/// Directory path segments that identify asset directories.
const DIRECTORY_SEGMENTS: &[&str] = &[
"playwright-report",
"test-results",
"cypress/videos",
"cypress/screenshots",
];
/// Filename glob patterns for individual asset files.
const FILENAME_GLOBS: &[&str] = &["junit*.xml", "*.trace.zip"];
/// Directories to exclude from the find search.
const EXCLUDE_DIRS: &[&str] = &[
".git",
@ -51,17 +32,18 @@ const EXCLUDE_DIRS: &[&str] = &[
"__pycache__",
];
/// Path segments that indicate excluded tool cache directories.
const EXCLUDE_SEGMENTS: &[&str] = &[".cache/ms-playwright", "playwright/.cache", ".yarn/cache"];
/// Maximum size for a single file (10 MB).
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.
pub fn build_find_command(root: &str, platform: &str) -> String {
/// Build a platform-aware find command to discover asset 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
/// and the remainder is matched via `-path '*/{dir}/*'`.
pub fn build_find_command(root: &str, platform: &str, globs: &[String]) -> String {
let mut cmd = format!("find {root}");
// Prune excluded directories
@ -73,17 +55,21 @@ pub fn build_find_command(root: &str, platform: &str) -> String {
cmd.push_str(&prune_parts.join(" -o "));
cmd.push_str(" \\) -prune -o");
// Match conditions: not a symlink, is a file, matches asset patterns
// Match conditions: not a symlink, is a file, matches user globs
cmd.push_str(" -not -type l -type f \\(");
let mut path_conditions: Vec<String> = Vec::new();
for segment in DIRECTORY_SEGMENTS {
path_conditions.push(format!(" -path '*/{segment}/*'"));
let mut conditions: Vec<String> = Vec::new();
for glob in globs {
if glob.contains('/') {
// Directory-style glob: strip trailing /** and match as path
let dir = glob.trim_end_matches("/**").trim_end_matches("/*");
conditions.push(format!(" -path '*/{dir}/*'"));
} else {
// Filename glob
conditions.push(format!(" -name '{glob}'"));
}
}
for glob in FILENAME_GLOBS {
path_conditions.push(format!(" -name '{glob}'"));
}
cmd.push_str(&path_conditions.join(" -o"));
cmd.push_str(&conditions.join(" -o"));
cmd.push_str(" \\)");
// Platform-specific output format
@ -177,94 +163,14 @@ fn parse_find_output_darwin(output: &str) -> Vec<DiscoveredFile> {
files
}
/// Check whether a path matches known asset patterns.
pub fn is_asset_candidate(path: &str) -> bool {
// Check excluded segments first
for seg in EXCLUDE_SEGMENTS {
if path.contains(seg) {
return false;
}
}
// Check directory segments — must appear as a complete path segment
for segment in DIRECTORY_SEGMENTS {
// segment may contain a slash (e.g., "cypress/videos"), so check
// that it appears bounded by / or start/end of string
if let Some(pos) = path.find(segment) {
let before_ok = pos == 0 || path.as_bytes()[pos - 1] == b'/';
let after_pos = pos + segment.len();
let after_ok = after_pos >= path.len() || path.as_bytes()[after_pos] == b'/';
if before_ok && after_ok {
return true;
}
}
}
// Check filename globs against the last path component
if let Some(filename) = path.rsplit('/').next() {
for glob_pattern in FILENAME_GLOBS {
if matches_simple_glob(glob_pattern, filename) {
return true;
}
}
// Also check at root level (no slash in path)
if !path.contains('/') {
for glob_pattern in FILENAME_GLOBS {
if matches_simple_glob(glob_pattern, path) {
return true;
}
}
}
}
false
}
/// Simple glob matching supporting only `*` wildcard.
fn matches_simple_glob(pattern: &str, text: &str) -> bool {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 1 {
return pattern == text;
}
// Check prefix
if !text.starts_with(parts[0]) {
return false;
}
// Check suffix
if !text.ends_with(parts[parts.len() - 1]) {
return false;
}
// For patterns like "junit*.xml", verify the middle parts appear in order
let mut pos = parts[0].len();
for part in &parts[1..parts.len() - 1] {
if let Some(found) = text[pos..].find(part) {
pos += found + part.len();
} else {
return false;
}
}
true
}
/// Select which files should be collected based on fingerprint changes, timing, and size budgets.
/// Select which files should be collected based on timing and size budgets.
pub fn select_files_to_collect(
discovered: &[DiscoveredFile],
baseline: &HashMap<String, FileFingerprint>,
command_start_epoch: f64,
) -> Vec<DiscoveredFile> {
let mut candidates: Vec<DiscoveredFile> = discovered
.iter()
.filter(|f| {
// Skip files that haven't changed since baseline
if let Some(fp) = baseline.get(&f.relative_path) {
if fp.size == f.size && (fp.mtime_epoch_secs - f.mtime_epoch_secs).abs() < 0.01 {
return false;
}
}
// Skip files older than command start
if f.mtime_epoch_secs < command_start_epoch {
return false;
@ -326,62 +232,27 @@ fn normalize_paths(discovered: Vec<DiscoveredFile>, root: &str) -> Vec<Discovere
.collect()
}
/// Take a snapshot of current asset files in the sandbox.
/// Returns a fingerprint map of discovered files.
pub async fn snapshot(sandbox: &dyn Sandbox) -> Result<HashMap<String, FileFingerprint>, String> {
let root = sandbox.working_directory();
let platform = sandbox.platform();
let cmd = build_find_command(root, platform);
debug!("Taking asset snapshot");
let result = sandbox
.exec_command(&cmd, FIND_TIMEOUT_MS, None, None, None)
.await?;
// Ignore non-zero exit codes — find may return 1 if some dirs are unreadable
let discovered = parse_find_output(&result.stdout, platform);
let discovered = normalize_paths(discovered, root);
let mut fingerprints = HashMap::new();
for f in discovered {
if is_asset_candidate(&f.relative_path) {
fingerprints.insert(
f.relative_path,
FileFingerprint {
size: f.size,
mtime_epoch_secs: f.mtime_epoch_secs,
},
);
}
}
Ok(fingerprints)
}
/// Collect asset files that changed since the baseline snapshot.
/// Collect asset files matching the configured globs that were created during this stage.
pub async fn collect_assets(
sandbox: &dyn Sandbox,
stage_dir: &Path,
baseline: &HashMap<String, FileFingerprint>,
globs: &[String],
command_start_epoch: f64,
) -> Result<AssetCollectionSummary, String> {
let root = sandbox.working_directory();
let platform = sandbox.platform();
let cmd = build_find_command(root, platform);
let cmd = build_find_command(root, platform, globs);
debug!(cmd = cmd.as_str(), "Collecting assets");
let result = sandbox
.exec_command(&cmd, FIND_TIMEOUT_MS, None, None, None)
.await?;
let discovered = parse_find_output(&result.stdout, platform);
let discovered = normalize_paths(discovered, root);
let candidates: Vec<DiscoveredFile> = discovered
.into_iter()
.filter(|f| is_asset_candidate(&f.relative_path))
.collect();
let total_discovered = candidates.len();
let to_collect = select_files_to_collect(&candidates, baseline, command_start_epoch);
let total_discovered = discovered.len();
let to_collect = select_files_to_collect(&discovered, command_start_epoch);
let files_skipped = total_discovered - to_collect.len();
let mut files_copied: usize = 0;
@ -437,6 +308,7 @@ pub async fn collect_assets(
mod tests {
use super::*;
use arc_agent::sandbox::ExecResult;
use std::collections::HashMap;
/// Minimal mock sandbox for asset_snapshot tests.
struct AssetMockSandbox {
@ -546,50 +418,6 @@ mod tests {
}
}
#[test]
fn is_asset_candidate_matches_directory_segments() {
assert!(is_asset_candidate("playwright-report/index.html"));
}
#[test]
fn is_asset_candidate_matches_nested_segments() {
assert!(is_asset_candidate("frontend/playwright-report/index.html"));
}
#[test]
fn is_asset_candidate_rejects_partial_segments() {
assert!(!is_asset_candidate("playwright-reporter/index.html"));
}
#[test]
fn is_asset_candidate_matches_filename_globs() {
assert!(is_asset_candidate("junit-report.xml"));
assert!(is_asset_candidate("some/dir/junit.xml"));
assert!(is_asset_candidate("output/results.trace.zip"));
}
#[test]
fn is_asset_candidate_rejects_excluded_paths() {
assert!(!is_asset_candidate(
".cache/ms-playwright/chromium/file.txt"
));
assert!(!is_asset_candidate("playwright/.cache/some-file"));
assert!(!is_asset_candidate(".yarn/cache/something.zip"));
}
#[test]
fn is_asset_candidate_matches_cypress_directories() {
assert!(is_asset_candidate("cypress/videos/test.mp4"));
assert!(is_asset_candidate("cypress/screenshots/fail.png"));
}
#[test]
fn is_asset_candidate_rejects_unrelated_paths() {
assert!(!is_asset_candidate("src/main.rs"));
assert!(!is_asset_candidate("package.json"));
assert!(!is_asset_candidate("report.xml"));
}
#[test]
fn parse_find_output_linux() {
let output = "1024\t1709312400.0\ttest-results/r.xml\n";
@ -620,25 +448,6 @@ mod tests {
assert_eq!(files[0].relative_path, "test-results/good.xml");
}
#[test]
fn select_files_skips_unchanged() {
let discovered = vec![DiscoveredFile {
relative_path: "test-results/r.xml".to_string(),
size: 1024,
mtime_epoch_secs: 1000.0,
}];
let mut baseline = HashMap::new();
baseline.insert(
"test-results/r.xml".to_string(),
FileFingerprint {
size: 1024,
mtime_epoch_secs: 1000.0,
},
);
let selected = select_files_to_collect(&discovered, &baseline, 500.0);
assert_eq!(selected.len(), 0);
}
#[test]
fn select_files_skips_old_mtime() {
let discovered = vec![DiscoveredFile {
@ -646,8 +455,7 @@ mod tests {
size: 1024,
mtime_epoch_secs: 500.0,
}];
let baseline = HashMap::new();
let selected = select_files_to_collect(&discovered, &baseline, 1000.0);
let selected = select_files_to_collect(&discovered, 1000.0);
assert_eq!(selected.len(), 0);
}
@ -658,8 +466,7 @@ mod tests {
size: MAX_FILE_SIZE + 1,
mtime_epoch_secs: 2000.0,
}];
let baseline = HashMap::new();
let selected = select_files_to_collect(&discovered, &baseline, 1000.0);
let selected = select_files_to_collect(&discovered, 1000.0);
assert_eq!(selected.len(), 0);
}
@ -682,8 +489,7 @@ mod tests {
mtime_epoch_secs: 2000.0,
},
];
let baseline = HashMap::new();
let selected = select_files_to_collect(&discovered, &baseline, 1000.0);
let selected = select_files_to_collect(&discovered, 1000.0);
assert_eq!(selected.len(), 3);
assert_eq!(selected[0].size, 1000);
assert_eq!(selected[1].size, 2000);
@ -699,28 +505,46 @@ mod tests {
mtime_epoch_secs: 2000.0,
})
.collect();
let baseline = HashMap::new();
let selected = select_files_to_collect(&discovered, &baseline, 1000.0);
let selected = select_files_to_collect(&discovered, 1000.0);
// 50 MB budget / 9 MB each = 5 fit (45 MB), 6th would be 54 MB
assert_eq!(selected.len(), 5);
}
#[test]
fn build_find_command_linux() {
let cmd = build_find_command("/workspace", "linux");
fn build_find_command_filename_glob() {
let globs = vec!["*.trace.zip".to_string()];
let cmd = build_find_command("/workspace", "linux", &globs);
assert!(cmd.contains("-name '*.trace.zip'"));
assert!(cmd.contains("-printf"));
assert!(cmd.contains("playwright-report"));
assert!(cmd.contains("junit*.xml"));
assert!(cmd.contains("-prune"));
assert!(cmd.contains("node_modules"));
}
#[test]
fn build_find_command_directory_glob() {
let globs = vec!["test-results/**".to_string()];
let cmd = build_find_command("/workspace", "linux", &globs);
assert!(cmd.contains("-path '*/test-results/*'"));
}
#[test]
fn build_find_command_mixed_globs() {
let globs = vec![
"test-results/**".to_string(),
"playwright-report/**".to_string(),
"*.trace.zip".to_string(),
];
let cmd = build_find_command("/workspace", "linux", &globs);
assert!(cmd.contains("-path '*/test-results/*'"));
assert!(cmd.contains("-path '*/playwright-report/*'"));
assert!(cmd.contains("-name '*.trace.zip'"));
}
#[test]
fn build_find_command_darwin() {
let cmd = build_find_command("/workspace", "darwin");
let globs = vec!["test-results/**".to_string()];
let cmd = build_find_command("/workspace", "darwin", &globs);
assert!(cmd.contains("-exec stat -f"));
assert!(cmd.contains("playwright-report"));
assert!(cmd.contains("junit*.xml"));
assert!(!cmd.contains("-printf"));
}
@ -749,21 +573,6 @@ mod tests {
assert_eq!(normalized[2].relative_path, "test-results/t.xml");
}
#[tokio::test]
async fn snapshot_uses_exec_command_and_parses() {
let mock = AssetMockSandbox::new(
HashMap::new(),
"1024\t2000.0\ttest-results/r.xml\n512\t2000.0\tsrc/main.rs\n",
"linux",
);
let fingerprints = snapshot(&mock).await.unwrap();
// Only test-results/r.xml is an asset candidate, src/main.rs is not
assert_eq!(fingerprints.len(), 1);
assert!(fingerprints.contains_key("test-results/r.xml"));
assert_eq!(fingerprints["test-results/r.xml"].size, 1024);
}
#[tokio::test]
async fn collect_assets_downloads_and_writes_manifest() {
let stage_dir = tempfile::tempdir().unwrap();
@ -773,8 +582,8 @@ mod tests {
let mock = AssetMockSandbox::new(files, "1024\t2000.0\ttest-results/r.xml\n", "linux");
let baseline = HashMap::new();
let summary = collect_assets(&mock, stage_dir.path(), &baseline, 1000.0)
let globs = vec!["test-results/**".to_string()];
let summary = collect_assets(&mock, stage_dir.path(), &globs, 1000.0)
.await
.unwrap();
@ -795,25 +604,17 @@ mod tests {
}
#[tokio::test]
async fn collect_assets_skips_unchanged_files() {
async fn collect_assets_skips_old_files() {
let stage_dir = tempfile::tempdir().unwrap();
let mut files = HashMap::new();
files.insert("test-results/r.xml".to_string(), "<test/>".to_string());
let mock = AssetMockSandbox::new(files, "1024\t2000.0\ttest-results/r.xml\n", "linux");
// File mtime (500.0) is before command_start_epoch (1000.0)
let mock = AssetMockSandbox::new(files, "1024\t500.0\ttest-results/r.xml\n", "linux");
// Provide a baseline with the same fingerprint
let mut baseline = HashMap::new();
baseline.insert(
"test-results/r.xml".to_string(),
FileFingerprint {
size: 1024,
mtime_epoch_secs: 2000.0,
},
);
let summary = collect_assets(&mock, stage_dir.path(), &baseline, 1000.0)
let globs = vec!["test-results/**".to_string()];
let summary = collect_assets(&mock, stage_dir.path(), &globs, 1000.0)
.await
.unwrap();
@ -831,8 +632,8 @@ mod tests {
"linux",
);
let baseline = HashMap::new();
let summary = collect_assets(&mock, stage_dir.path(), &baseline, 1000.0)
let globs = vec!["test-results/**".to_string()];
let summary = collect_assets(&mock, stage_dir.path(), &globs, 1000.0)
.await
.unwrap();

View file

@ -770,6 +770,11 @@ pub async fn run_command(
.as_ref()
.and_then(|c| c.pull_request.as_ref())
.is_some_and(|p| p.enabled),
asset_globs: run_cfg
.as_ref()
.and_then(|c| c.assets.as_ref())
.map(|a| a.include.clone())
.unwrap_or_default(),
};
let run_start = Instant::now();
@ -1204,6 +1209,7 @@ async fn run_from_branch(
git_author,
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let run_start = Instant::now();
@ -1764,6 +1770,7 @@ mod tests {
hooks: Vec::new(),
checkpoint: Default::default(),
pull_request: None,
assets: None,
};
let (model, provider) = resolve_model_provider(
Some("gpt-5.2"),
@ -1806,6 +1813,7 @@ mod tests {
hooks: Vec::new(),
checkpoint: Default::default(),
pull_request: None,
assets: None,
};
let (model, provider) = resolve_model_provider(None, None, Some(&cfg), &defaults, &graph);
assert_eq!(model, "toml-model");
@ -1883,6 +1891,7 @@ mod tests {
hooks: Vec::new(),
checkpoint: Default::default(),
pull_request: None,
assets: None,
};
let (model, provider) = resolve_model_provider(None, None, Some(&cfg), &defaults, &graph);
assert_eq!(model, "toml-model");
@ -1909,6 +1918,7 @@ mod tests {
hooks: Vec::new(),
checkpoint: Default::default(),
pull_request: None,
assets: None,
};
let defaults = RunDefaults::default();
assert!(resolve_preserve_sandbox(true, Some(&cfg), &defaults));
@ -1934,6 +1944,7 @@ mod tests {
hooks: Vec::new(),
checkpoint: Default::default(),
pull_request: None,
assets: None,
};
let defaults = RunDefaults {
sandbox: Some(run_config::SandboxConfig {

View file

@ -22,6 +22,12 @@ pub struct PullRequestConfig {
pub enabled: bool,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct AssetsConfig {
#[serde(default)]
pub include: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WorkflowRunConfig {
@ -38,6 +44,7 @@ pub struct WorkflowRunConfig {
#[serde(default)]
pub checkpoint: CheckpointConfig,
pub pull_request: Option<PullRequestConfig>,
pub assets: Option<AssetsConfig>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
@ -76,6 +83,7 @@ pub struct RunDefaults {
#[serde(default)]
pub checkpoint: CheckpointConfig,
pub pull_request: Option<PullRequestConfig>,
pub assets: Option<AssetsConfig>,
}
impl WorkflowRunConfig {
@ -176,6 +184,10 @@ impl WorkflowRunConfig {
if self.pull_request.is_none() {
self.pull_request = defaults.pull_request.clone();
}
if self.assets.is_none() {
self.assets = defaults.assets.clone();
}
}
}
@ -1790,4 +1802,77 @@ enabled = true
let defaults: RunDefaults = toml::from_str(toml).unwrap();
assert!(defaults.pull_request.unwrap().enabled);
}
#[test]
fn parse_toml_with_assets() {
let toml = r#"
version = 1
goal = "Run tests"
graph = "workflow.dot"
[assets]
include = ["test-results/**", "playwright-report/**", "*.trace.zip"]
"#;
let config = parse_run_config(toml).unwrap();
let assets = config.assets.unwrap();
assert_eq!(
assets.include,
vec!["test-results/**", "playwright-report/**", "*.trace.zip"]
);
}
#[test]
fn parse_toml_without_assets() {
let toml = r#"
version = 1
graph = "workflow.dot"
"#;
let config = parse_run_config(toml).unwrap();
assert!(config.assets.is_none());
}
#[test]
fn apply_defaults_inherits_assets() {
let mut cfg = parse_run_config(
r#"
version = 1
goal = "test"
graph = "w.dot"
"#,
)
.unwrap();
let defaults = RunDefaults {
assets: Some(AssetsConfig {
include: vec!["test-results/**".into()],
}),
..RunDefaults::default()
};
cfg.apply_defaults(&defaults);
let assets = cfg.assets.unwrap();
assert_eq!(assets.include, vec!["test-results/**"]);
}
#[test]
fn apply_defaults_task_assets_wins() {
let mut cfg = parse_run_config(
r#"
version = 1
goal = "test"
graph = "w.dot"
[assets]
include = ["playwright-report/**"]
"#,
)
.unwrap();
let defaults = RunDefaults {
assets: Some(AssetsConfig {
include: vec!["test-results/**".into()],
}),
..RunDefaults::default()
};
cfg.apply_defaults(&defaults);
let assets = cfg.assets.unwrap();
assert_eq!(assets.include, vec!["playwright-report/**"]);
}
}

View file

@ -855,6 +855,8 @@ pub struct RunConfig {
pub base_branch: Option<String>,
/// Whether to auto-create a PR on successful completion.
pub pull_request_enabled: bool,
/// Glob patterns for asset collection. Empty = no asset collection.
pub asset_globs: Vec<String>,
}
/// The workflow run execution engine.
@ -983,20 +985,13 @@ impl WorkflowRunEngine {
policy: &RetryPolicy,
stage_index: usize,
visit: usize,
asset_globs: &[String],
) -> Result<(Outcome, u32)> {
let handler = self.services.registry.resolve(node);
let node_timeout = node.timeout();
for attempt in 1..=policy.max_attempts {
// Take baseline asset snapshot before handler execution
let baseline = match asset_snapshot::snapshot(self.services.sandbox.as_ref()).await {
Ok(fp) => fp,
Err(e) => {
tracing::warn!(node = %node.id, error = %e, "Asset baseline snapshot failed");
std::collections::HashMap::new()
}
};
// Floor to integer seconds: macOS stat reports mtime as integer seconds,
// so a fractional epoch would reject files created in the same second.
let command_start_epoch = std::time::SystemTime::now()
@ -1038,8 +1033,8 @@ impl WorkflowRunEngine {
}
};
// Collect assets after handler completes (both success and error)
{
// Collect assets after handler completes (only when globs are configured)
if !asset_globs.is_empty() {
let node_slug = if visit <= 1 {
node.id.clone()
} else {
@ -1053,7 +1048,7 @@ impl WorkflowRunEngine {
match asset_snapshot::collect_assets(
self.services.sandbox.as_ref(),
&assets_dir,
&baseline,
asset_globs,
command_start_epoch,
)
.await
@ -1636,7 +1631,7 @@ impl WorkflowRunEngine {
let (mut outcome, attempts_used) = if let Some((ref token, _)) = stall_token {
tokio::select! {
result = self.execute_with_retry(
node, &context, graph, &config.logs_root, &retry_policy, stage_index, visit,
node, &context, graph, &config.logs_root, &retry_policy, stage_index, visit, &config.asset_globs,
) => result?,
() = token.cancelled() => {
let idle_secs = graph.stall_timeout().map_or(0, |d| d.as_secs());
@ -1659,6 +1654,7 @@ impl WorkflowRunEngine {
&retry_policy,
stage_index,
visit,
&config.asset_globs,
)
.await?
};
@ -2878,6 +2874,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -2904,6 +2901,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
let checkpoint_path = dir.path().join("checkpoint.json");
@ -2938,6 +2936,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -2968,6 +2967,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -2994,6 +2994,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3033,6 +3034,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -3096,6 +3098,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3186,6 +3189,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3219,6 +3223,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3247,6 +3252,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3275,6 +3281,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3307,6 +3314,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3467,6 +3475,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3510,6 +3519,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();
@ -3571,6 +3581,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
@ -3635,6 +3646,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
@ -3703,6 +3715,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_ok());
@ -3760,6 +3773,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -3818,6 +3832,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
@ -3851,6 +3866,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -3880,6 +3896,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -3908,6 +3925,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -3949,6 +3967,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
// Set cancel after a short delay (while the slow handler is running)
@ -4027,6 +4046,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4058,6 +4078,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4091,6 +4112,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4129,6 +4151,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4165,6 +4188,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4198,6 +4222,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4292,6 +4317,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
// The engine returns Err because the Fail outcome has no outgoing fail edge,
@ -4501,6 +4527,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4537,6 +4564,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4580,6 +4608,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4663,6 +4692,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4757,6 +4787,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4828,6 +4859,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -4886,6 +4918,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -4945,6 +4978,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let _outcome = engine.run(&g, &config).await.unwrap();
@ -5031,6 +5065,7 @@ mod tests {
git_author: crate::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&g, &config).await.unwrap();

View file

@ -153,6 +153,7 @@ impl Handler for SubWorkflowHandler {
.unwrap_or_default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
// Clone parent context for child; inject parent preamble

View file

@ -400,6 +400,7 @@ async fn daytona_pipeline_artifact_offload_and_sync() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -596,6 +597,7 @@ async fn daytona_git_checkpoint_remote_emits_events() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -784,6 +786,7 @@ async fn daytona_parallel_git_branching_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1160,6 +1163,7 @@ async fn daytona_git_checkpoint_with_shadow_branch() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1302,6 +1306,7 @@ async fn daytona_asset_collection() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1558,6 +1563,7 @@ async fn daytona_git_push_run_branch_to_origin() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine

View file

@ -204,6 +204,7 @@ async fn end_to_end_linear_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -346,6 +347,7 @@ async fn end_to_end_branching_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -470,6 +472,7 @@ async fn end_to_end_human_gate_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -584,6 +587,7 @@ async fn goal_gate_routes_to_retry_target_on_failure() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -708,6 +712,7 @@ async fn goal_gate_routes_to_retry_target_when_present() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1019,6 +1024,7 @@ async fn retry_on_failure_then_succeed() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1097,6 +1103,7 @@ async fn pipeline_with_many_nodes() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1425,6 +1432,7 @@ async fn smoke_test_with_mock_codergen_backend() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1529,6 +1537,7 @@ async fn end_to_end_parallel_fan_out_fan_in() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1645,6 +1654,7 @@ async fn resume_from_checkpoint_completes_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -1747,6 +1757,7 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
// This should succeed because goal gate for gated_work is satisfied
@ -1793,6 +1804,7 @@ async fn graph_goal_in_context() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -1831,6 +1843,7 @@ async fn event_streaming_lifecycle() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -1913,6 +1926,7 @@ async fn context_flow_between_stages() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -1968,6 +1982,7 @@ async fn tool_handler_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2040,6 +2055,7 @@ async fn auto_approve_interviewer_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2079,6 +2095,7 @@ async fn codergen_without_backend_simulated() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -2186,6 +2203,7 @@ async fn branching_loop_back_on_failure() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2273,6 +2291,7 @@ async fn human_gate_loops_back() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2333,6 +2352,7 @@ async fn scenario_ship_a_feature() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2421,6 +2441,7 @@ async fn scenario_parallel_expert_review() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2503,6 +2524,7 @@ async fn scenario_node_retries_on_retry_status() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2567,6 +2589,7 @@ async fn scenario_loop_restart_resets_context() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2637,6 +2660,7 @@ async fn scenario_bug_triage_router() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -2698,6 +2722,7 @@ async fn scenario_crash_recovery() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
.run_from_checkpoint(&graph, &config, &checkpoint)
@ -2809,6 +2834,7 @@ async fn manager_loop_stop_condition_satisfied_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
@ -2888,6 +2914,7 @@ async fn manager_loop_max_cycles_exceeded_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
@ -3026,6 +3053,7 @@ async fn conditional_branching_success_fail_paths() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -3081,6 +3109,7 @@ async fn edge_selection_condition_match_wins_over_weight() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -3130,6 +3159,7 @@ async fn edge_selection_weight_breaks_ties() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -3171,6 +3201,7 @@ async fn edge_selection_lexical_tiebreak() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -3231,6 +3262,7 @@ async fn context_updates_visible_across_nodes() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -3277,6 +3309,7 @@ async fn stylesheet_applies_model_override() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -3335,6 +3368,7 @@ async fn custom_handler_registration_and_execution() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -3408,6 +3442,7 @@ async fn integration_smoke_plan_implement_review_done() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);
@ -3514,6 +3549,7 @@ async fn manager_loop_runs_child_engine_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -3651,6 +3687,7 @@ async fn manager_loop_context_flows_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
@ -3727,6 +3764,7 @@ async fn manager_loop_child_dotfile_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
@ -3843,6 +3881,7 @@ async fn graph_merge_e2e_through_engine() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -3996,6 +4035,7 @@ async fn fidelity_default_is_compact() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4055,6 +4095,7 @@ async fn fidelity_graph_default_applied() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4110,6 +4151,7 @@ async fn fidelity_node_overrides_graph_default() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4171,6 +4213,7 @@ async fn fidelity_edge_overrides_node_and_graph() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4222,6 +4265,7 @@ async fn fidelity_full_produces_empty_preamble() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4283,6 +4327,7 @@ async fn fidelity_truncate_preamble_minimal() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4357,6 +4402,7 @@ async fn fidelity_summary_low_mode() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4426,6 +4472,7 @@ async fn fidelity_summary_medium_mode() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4495,6 +4542,7 @@ async fn fidelity_summary_high_mode() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4557,6 +4605,7 @@ async fn fidelity_full_sets_thread_id_in_context() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4630,6 +4679,7 @@ async fn fidelity_full_nodes_share_thread_id() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -4713,6 +4763,7 @@ async fn fidelity_resume_degrades_full_to_summary_high() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine
.run_from_checkpoint(&graph, &config, &checkpoint)
@ -4812,6 +4863,7 @@ async fn fidelity_resume_degrade_only_affects_first_hop() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine
.run_from_checkpoint(&graph, &config, &checkpoint)
@ -4898,6 +4950,7 @@ async fn fidelity_resume_no_degrade_when_not_full() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine
.run_from_checkpoint(&graph, &config, &checkpoint)
@ -4942,6 +4995,7 @@ async fn fidelity_stored_in_checkpoint_context() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5030,6 +5084,7 @@ async fn fidelity_precedence_multi_node_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5100,6 +5155,7 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5178,6 +5234,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine_low
.run(&graph_low, &config_low)
@ -5248,6 +5305,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine_med
.run(&graph_med, &config_med)
@ -5321,6 +5379,7 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5377,6 +5436,7 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5436,6 +5496,7 @@ async fn fidelity_edge_thread_id_override_in_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5496,6 +5557,7 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5566,6 +5628,7 @@ async fn fidelity_from_parsed_dot_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5616,6 +5679,7 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5688,6 +5752,7 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine.run(&graph, &config).await.expect("run");
@ -5777,6 +5842,7 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine
.run_from_checkpoint(&graph, &config, &checkpoint)
@ -5990,6 +6056,7 @@ mod real_llm {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = tokio::time::timeout(
@ -6107,6 +6174,7 @@ mod real_llm {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = tokio::time::timeout(
@ -6249,6 +6317,7 @@ mod real_llm {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = tokio::time::timeout(
@ -6359,6 +6428,7 @@ mod real_llm {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = tokio::time::timeout(
@ -6458,6 +6528,7 @@ async fn human_gate_freeform_only_routes_text() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -6592,6 +6663,7 @@ async fn human_gate_freeform_with_fixed_choice_match() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -6710,6 +6782,7 @@ async fn human_gate_freeform_fallback_on_unmatched_text() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -6842,6 +6915,7 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -6954,6 +7028,7 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -7217,6 +7292,7 @@ fn make_run_config(dir: &std::path::Path) -> RunConfig {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
}
}
@ -8356,6 +8432,7 @@ async fn arc_e2e_with_real_llm() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -8487,6 +8564,7 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
engine
@ -8689,6 +8767,7 @@ async fn large_context_values_are_offloaded_to_artifact_store() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -8904,6 +8983,7 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -9037,6 +9117,7 @@ async fn node_dir_uses_visit_count_on_revisit() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -9993,6 +10074,7 @@ async fn full_pipeline_with_cli_backend_node() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -10125,6 +10207,7 @@ async fn stylesheet_backend_property_routes_to_cli() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -10410,6 +10493,7 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
// 5. Run pipeline
@ -10602,6 +10686,7 @@ async fn git_checkpoint_host_writes_shadow_branch() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
// 5. Run pipeline
@ -10799,6 +10884,7 @@ async fn parallel_git_branching_host_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
// 5. Run pipeline
@ -11064,6 +11150,7 @@ async fn git_checkpoint_host_skips_empty_diff_patch() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -11449,6 +11536,7 @@ async fn e2e_circuit_breaker_deterministic_self_loop() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -11499,6 +11587,7 @@ async fn e2e_circuit_breaker_custom_limit() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -11542,6 +11631,7 @@ async fn e2e_circuit_breaker_ignores_transient_failures() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -11592,6 +11682,7 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -11635,6 +11726,7 @@ async fn e2e_circuit_breaker_loop_restart() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -11700,6 +11792,7 @@ async fn e2e_failure_signature_persisted_in_context() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.unwrap();
@ -11767,6 +11860,7 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let _outcome = engine.run(&graph, &config).await.unwrap();
@ -11826,6 +11920,7 @@ async fn e2e_signature_maps_persist_in_checkpoint() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.unwrap();
@ -11956,6 +12051,7 @@ async fn e2e_circuit_breaker_emits_events_before_abort() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12026,6 +12122,7 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.unwrap();
@ -12125,6 +12222,7 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12224,6 +12322,7 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12267,6 +12366,7 @@ async fn e2e_loop_restart_blocked_for_structural_failure() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12310,6 +12410,7 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12353,6 +12454,7 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12393,6 +12495,7 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12437,6 +12540,7 @@ async fn e2e_loop_restart_allowed_for_transient_infra() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12544,6 +12648,7 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let result = engine.run(&graph, &config).await;
@ -12603,6 +12708,7 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -12652,6 +12758,7 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -12720,6 +12827,7 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let start = std::time::Instant::now();
@ -12853,6 +12961,7 @@ async fn asset_collection_local_sandbox_success() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -12964,6 +13073,7 @@ async fn asset_collection_local_sandbox_on_failure() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -13058,6 +13168,7 @@ async fn asset_collection_docker_sandbox() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine
@ -13130,6 +13241,7 @@ async fn wait_timer_e2e() {
git_author: arc_workflows::git::GitAuthor::default(),
base_branch: None,
pull_request_enabled: false,
asset_globs: Vec::new(),
};
let outcome = engine.run(&graph, &config).await.expect("run");
assert_eq!(outcome.status, StageStatus::Success);