diff --git a/crates/arc-api/src/demo/mod.rs b/crates/arc-api/src/demo/mod.rs index 496e61c54..a15ee7636 100644 --- a/crates/arc-api/src/demo/mod.rs +++ b/crates/arc-api/src/demo/mod.rs @@ -969,6 +969,7 @@ mod runs { ("branch".into(), "feature/rate-limiting".into()), ])), hooks: vec![], + checkpoint: Default::default(), }) .unwrap() } @@ -1088,6 +1089,7 @@ mod workflows { ("branch".into(), "main".into()), ])), hooks: vec![], + checkpoint: Default::default(), }), graph: r#"digraph fix_build { graph [ @@ -1152,6 +1154,7 @@ mod workflows { ("test_framework".into(), "vitest".into()), ])), hooks: vec![], + checkpoint: Default::default(), }), graph: r#"digraph implement { graph [ @@ -1228,6 +1231,7 @@ mod workflows { ("drift_threshold".into(), "warn".into()), ])), hooks: vec![], + checkpoint: Default::default(), }), graph: r#"digraph sync { graph [ @@ -1293,6 +1297,7 @@ mod workflows { ("min_confidence".into(), "0.8".into()), ])), hooks: vec![], + checkpoint: Default::default(), }), graph: r#"digraph expand { graph [ @@ -2680,6 +2685,7 @@ mod settings { exe: None, }), vars: None, + checkpoint: Default::default(), }, hook_config: arc_workflows::hook::HookConfig { hooks: vec![], diff --git a/crates/arc-api/src/server.rs b/crates/arc-api/src/server.rs index c49da7f3a..6da9b741b 100644 --- a/crates/arc-api/src/server.rs +++ b/crates/arc-api/src/server.rs @@ -577,6 +577,7 @@ async fn execute_run(state: Arc, run_id: String) { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = tokio::select! { diff --git a/crates/arc-api/src/server_config.rs b/crates/arc-api/src/server_config.rs index cc67a4368..ea958836d 100644 --- a/crates/arc-api/src/server_config.rs +++ b/crates/arc-api/src/server_config.rs @@ -446,4 +446,24 @@ matcher = "agent_loop" let config: ServerConfig = toml::from_str(toml).unwrap(); assert!(config.hook_config.hooks.is_empty()); } + + #[test] + fn parse_config_with_checkpoint_exclude_globs() { + let toml = r#" +[checkpoint] +exclude_globs = ["**/node_modules/**", "**/.cache/**"] +"#; + let config: ServerConfig = toml::from_str(toml).unwrap(); + assert_eq!( + config.run_defaults.checkpoint.exclude_globs, + vec!["**/node_modules/**", "**/.cache/**"] + ); + } + + #[test] + fn parse_config_checkpoint_defaults_empty() { + let toml = ""; + let config: ServerConfig = toml::from_str(toml).unwrap(); + assert!(config.run_defaults.checkpoint.exclude_globs.is_empty()); + } } diff --git a/crates/arc-workflows/src/cli/run.rs b/crates/arc-workflows/src/cli/run.rs index 94bb3f8d9..e02fdceac 100644 --- a/crates/arc-workflows/src/cli/run.rs +++ b/crates/arc-workflows/src/cli/run.rs @@ -666,6 +666,10 @@ pub async fn run_command( } else { None }; + let checkpoint_exclude_globs = run_cfg + .as_ref() + .map(|c| c.checkpoint.exclude_globs.clone()) + .unwrap_or_default(); let config = RunConfig { logs_root: logs_dir.clone(), cancel_token: None, @@ -689,6 +693,7 @@ pub async fn run_command( .filter_map(|s| s.split_once('=')) .map(|(k, v)| (k.to_string(), v.to_string())) .collect(), + checkpoint_exclude_globs, }; let run_start = Instant::now(); @@ -1054,6 +1059,7 @@ async fn run_from_branch( run_branch: Some(run_branch.to_string()), meta_branch, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let run_start = Instant::now(); @@ -1538,6 +1544,7 @@ mod tests { sandbox: None, vars: None, hooks: Vec::new(), + checkpoint: Default::default(), }; let (model, provider) = resolve_model_provider( Some("gpt-5.2"), @@ -1578,6 +1585,7 @@ mod tests { sandbox: None, vars: None, hooks: Vec::new(), + checkpoint: Default::default(), }; let (model, provider) = resolve_model_provider(None, None, Some(&cfg), &defaults, &graph); assert_eq!(model, "toml-model"); @@ -1653,6 +1661,7 @@ mod tests { sandbox: None, vars: None, hooks: Vec::new(), + checkpoint: Default::default(), }; let (model, provider) = resolve_model_provider(None, None, Some(&cfg), &defaults, &graph); assert_eq!(model, "toml-model"); @@ -1676,6 +1685,7 @@ mod tests { }), vars: None, hooks: Vec::new(), + checkpoint: Default::default(), }; let defaults = RunDefaults::default(); assert!(resolve_preserve_sandbox(true, Some(&cfg), &defaults)); @@ -1698,6 +1708,7 @@ mod tests { }), vars: None, hooks: Vec::new(), + checkpoint: Default::default(), }; let defaults = RunDefaults { sandbox: Some(run_config::SandboxConfig { diff --git a/crates/arc-workflows/src/cli/run_config.rs b/crates/arc-workflows/src/cli/run_config.rs index 1ae9ede7d..306c34189 100644 --- a/crates/arc-workflows/src/cli/run_config.rs +++ b/crates/arc-workflows/src/cli/run_config.rs @@ -8,6 +8,12 @@ use crate::daytona_sandbox::DaytonaConfig; const SUPPORTED_VERSION: u32 = 1; +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct CheckpointConfig { + #[serde(default)] + pub exclude_globs: Vec, +} + #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct WorkflowRunConfig { @@ -21,6 +27,8 @@ pub struct WorkflowRunConfig { pub vars: Option>, #[serde(default)] pub hooks: Vec, + #[serde(default)] + pub checkpoint: CheckpointConfig, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -55,6 +63,8 @@ pub struct RunDefaults { pub setup: Option, pub sandbox: Option, pub vars: Option>, + #[serde(default)] + pub checkpoint: CheckpointConfig, } impl WorkflowRunConfig { @@ -135,6 +145,15 @@ impl WorkflowRunConfig { } self.vars = Some(merged); } + + // Union checkpoint exclude globs from defaults and task config, dedup + if !defaults.checkpoint.exclude_globs.is_empty() { + let mut merged = defaults.checkpoint.exclude_globs.clone(); + merged.extend(self.checkpoint.exclude_globs.drain(..)); + merged.sort(); + merged.dedup(); + self.checkpoint.exclude_globs = merged; + } } } @@ -1237,4 +1256,118 @@ auto_stop_interval = 60 ])) ); } + + #[test] + fn parse_toml_with_checkpoint_exclude_globs() { + let toml = r#" +version = 1 +goal = "test" +graph = "w.dot" + +[checkpoint] +exclude_globs = ["**/node_modules/**", "**/.cache/**"] +"#; + let config = parse_run_config(toml).unwrap(); + assert_eq!( + config.checkpoint.exclude_globs, + vec!["**/node_modules/**", "**/.cache/**"] + ); + } + + #[test] + fn parse_toml_without_checkpoint_defaults_empty() { + let toml = r#" +version = 1 +goal = "test" +graph = "w.dot" +"#; + let config = parse_run_config(toml).unwrap(); + assert!(config.checkpoint.exclude_globs.is_empty()); + } + + #[test] + fn apply_defaults_unions_checkpoint_exclude_globs() { + let mut cfg = parse_run_config( + r#" +version = 1 +goal = "test" +graph = "w.dot" + +[checkpoint] +exclude_globs = ["**/dist/**", "**/.cache/**"] +"#, + ) + .unwrap(); + let defaults = RunDefaults { + checkpoint: CheckpointConfig { + exclude_globs: vec![ + "**/.cache/**".into(), + "**/node_modules/**".into(), + ], + }, + ..RunDefaults::default() + }; + cfg.apply_defaults(&defaults); + assert_eq!( + cfg.checkpoint.exclude_globs, + vec!["**/.cache/**", "**/dist/**", "**/node_modules/**"] + ); + } + + #[test] + fn apply_defaults_checkpoint_from_defaults_only() { + let mut cfg = parse_run_config( + r#" +version = 1 +goal = "test" +graph = "w.dot" +"#, + ) + .unwrap(); + let defaults = RunDefaults { + checkpoint: CheckpointConfig { + exclude_globs: vec!["**/node_modules/**".into()], + }, + ..RunDefaults::default() + }; + cfg.apply_defaults(&defaults); + assert_eq!( + cfg.checkpoint.exclude_globs, + vec!["**/node_modules/**"] + ); + } + + #[test] + fn apply_defaults_checkpoint_from_task_only() { + let mut cfg = parse_run_config( + r#" +version = 1 +goal = "test" +graph = "w.dot" + +[checkpoint] +exclude_globs = ["**/dist/**"] +"#, + ) + .unwrap(); + let defaults = RunDefaults::default(); + cfg.apply_defaults(&defaults); + assert_eq!( + cfg.checkpoint.exclude_globs, + vec!["**/dist/**"] + ); + } + + #[test] + fn parse_run_defaults_with_checkpoint() { + let toml = r#" +[checkpoint] +exclude_globs = ["**/node_modules/**"] +"#; + let defaults: RunDefaults = toml::from_str(toml).unwrap(); + assert_eq!( + defaults.checkpoint.exclude_globs, + vec!["**/node_modules/**"] + ); + } } diff --git a/crates/arc-workflows/src/engine.rs b/crates/arc-workflows/src/engine.rs index 492451d9d..a7e4df1d0 100644 --- a/crates/arc-workflows/src/engine.rs +++ b/crates/arc-workflows/src/engine.rs @@ -520,6 +520,7 @@ pub struct GitState { pub base_sha: String, pub run_branch: Option, pub meta_branch: Option, + pub checkpoint_exclude_globs: Vec, } /// How git checkpointing should be performed for a workflow run. @@ -540,6 +541,7 @@ pub async fn git_checkpoint_host( status: String, completed_count: usize, shadow_sha: Option, + exclude_globs: Vec, ) -> Option { match tokio::task::spawn_blocking(move || { crate::git::checkpoint_commit( @@ -549,6 +551,7 @@ pub async fn git_checkpoint_host( &status, completed_count, shadow_sha.as_deref(), + &exclude_globs, ) }) .await @@ -583,9 +586,18 @@ pub async fn git_checkpoint_remote( status: &str, completed_count: usize, shadow_sha: Option, + exclude_globs: &[String], ) -> Option { - // Stage everything - let add_cmd = format!("{GIT_REMOTE} add -A"); + // Stage everything (with optional excludes) + let add_cmd = if exclude_globs.is_empty() { + format!("{GIT_REMOTE} add -A") + } else { + let pathspecs: Vec = exclude_globs + .iter() + .map(|g| format!("':(glob,exclude){g}'")) + .collect(); + format!("{GIT_REMOTE} add -A -- . {}", pathspecs.join(" ")) + }; let add_result = sandbox .exec_command(&add_cmd, 30_000, None, None, None) .await; @@ -744,6 +756,9 @@ pub struct RunConfig { pub meta_branch: Option, /// User-defined key-value labels for this run. pub labels: HashMap, + /// Glob patterns to exclude from git checkpoint staging. + #[allow(clippy::struct_field_names)] + pub checkpoint_exclude_globs: Vec, } /// The workflow run execution engine. @@ -1121,6 +1136,7 @@ impl WorkflowRunEngine { base_sha: base_sha.clone(), run_branch: config.run_branch.clone(), meta_branch: config.meta_branch.clone(), + checkpoint_exclude_globs: config.checkpoint_exclude_globs.clone(), })), _ => None, }; @@ -1843,6 +1859,7 @@ impl WorkflowRunEngine { status_str, completed_count, shadow_sha, + config.checkpoint_exclude_globs.clone(), ) .await } @@ -1854,6 +1871,7 @@ impl WorkflowRunEngine { &outcome.status.to_string(), completed_count, shadow_sha, + &config.checkpoint_exclude_globs, ) .await } @@ -2711,6 +2729,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -2732,6 +2751,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); let checkpoint_path = dir.path().join("checkpoint.json"); @@ -2761,6 +2781,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -2786,6 +2807,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -2807,6 +2829,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -2841,6 +2864,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -2899,6 +2923,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -2984,6 +3009,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -3012,6 +3038,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::from([("env".into(), "test".into())]), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -3036,6 +3063,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -3060,6 +3088,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -3087,6 +3116,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -3242,6 +3272,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -3280,6 +3311,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&g, &config).await.unwrap(); @@ -3336,6 +3368,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); @@ -3395,6 +3428,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; @@ -3458,6 +3492,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_ok()); @@ -3510,6 +3545,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -3563,6 +3599,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); @@ -3591,6 +3628,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -3615,6 +3653,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3638,6 +3677,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -3674,6 +3714,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; // Set cancel after a short delay (while the slow handler is running) @@ -3747,6 +3788,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3773,6 +3815,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3801,6 +3844,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3834,6 +3878,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3865,6 +3910,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3893,6 +3939,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3982,6 +4029,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; // The engine returns Err because the Fail outcome has no outgoing fail edge, @@ -4186,6 +4234,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4217,6 +4266,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4255,6 +4305,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4333,6 +4384,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4422,6 +4474,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4488,6 +4541,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -4541,6 +4595,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -4595,6 +4650,7 @@ mod tests { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let _outcome = engine.run(&g, &config).await.unwrap(); diff --git a/crates/arc-workflows/src/git.rs b/crates/arc-workflows/src/git.rs index 75d308650..6c63960b2 100644 --- a/crates/arc-workflows/src/git.rs +++ b/crates/arc-workflows/src/git.rs @@ -144,11 +144,21 @@ pub fn checkpoint_commit( status: &str, completed_count: usize, shadow_sha: Option<&str>, + excludes: &[String], ) -> Result { tracing::debug!(path = %work_dir.display(), node_id, "Creating git checkpoint commit"); - // Stage everything - let output = git_cmd(work_dir) - .args(["add", "-A"]) + // Stage everything (with optional excludes) + let mut cmd = git_cmd(work_dir); + cmd.args(["add", "-A", "--"]); + if excludes.is_empty() { + cmd.arg("."); + } else { + cmd.arg("."); + for glob in excludes { + cmd.arg(format!(":(glob,exclude){glob}")); + } + } + let output = cmd .output() .map_err(|e| git_error(format!("git add failed: {e}")))?; @@ -497,7 +507,7 @@ mod tests { let wt = dir.path().join("ff-wt"); add_worktree(dir.path(), &wt, "ff-branch").unwrap(); fs::write(wt.join("new.txt"), "data").unwrap(); - checkpoint_commit(&wt, "run", "node", "ok", 1, None).unwrap(); + checkpoint_commit(&wt, "run", "node", "ok", 1, None, &[]).unwrap(); let advanced_sha = head_sha(&wt).unwrap(); remove_worktree(dir.path(), &wt).unwrap(); @@ -569,7 +579,7 @@ mod tests { // Simulate a shadow commit SHA let shadow_sha = "abcdef1234567890abcdef1234567890abcdef12"; let sha = - checkpoint_commit(&wt_path, "run1", "nodeA", "success", 3, Some(shadow_sha)).unwrap(); + checkpoint_commit(&wt_path, "run1", "nodeA", "success", 3, Some(shadow_sha), &[]).unwrap(); assert_eq!(sha.len(), 40); assert!(sha.chars().all(|c| c.is_ascii_hexdigit())); @@ -608,7 +618,7 @@ mod tests { let wt_path = dir.path().join("worktree"); add_worktree(dir.path(), &wt_path, "run-branch2").unwrap(); - let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 1, None).unwrap(); + let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 1, None, &[]).unwrap(); assert_eq!(sha.len(), 40); // Verify Arc-Completed trailer present but no Arc-Meta @@ -652,7 +662,7 @@ mod tests { let wt_path = dir.path().join("worktree"); add_worktree(dir.path(), &wt_path, "fallback-branch").unwrap(); - let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 0, None).unwrap(); + let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 0, None, &[]).unwrap(); assert_eq!(sha.len(), 40); remove_worktree(dir.path(), &wt_path).unwrap(); @@ -921,4 +931,79 @@ mod tests { assert_eq!(head_sha(dir.path()).unwrap(), initial_sha); assert!(!dir.path().join("file.txt").exists()); } + + #[test] + fn checkpoint_commit_with_excludes_skips_matching_files() { + let dir = tempfile::tempdir().unwrap(); + init_repo(dir.path()); + create_branch(dir.path(), "excl-branch").unwrap(); + + let wt_path = dir.path().join("worktree"); + add_worktree(dir.path(), &wt_path, "excl-branch").unwrap(); + + // Create files: one should be staged, one excluded + fs::write(wt_path.join("kept.txt"), "keep me").unwrap(); + fs::create_dir_all(wt_path.join("node_modules/pkg")).unwrap(); + fs::write(wt_path.join("node_modules/pkg/index.js"), "module").unwrap(); + + let excludes = vec!["**/node_modules/**".to_string()]; + checkpoint_commit(&wt_path, "run", "node", "ok", 1, None, &excludes).unwrap(); + + // Verify kept.txt was committed + let output = Command::new("git") + .args(["show", "--name-only", "--format=", "HEAD"]) + .current_dir(&wt_path) + .output() + .unwrap(); + let committed_files = String::from_utf8_lossy(&output.stdout); + assert!( + committed_files.contains("kept.txt"), + "kept.txt should be committed" + ); + assert!( + !committed_files.contains("node_modules"), + "node_modules should be excluded" + ); + + remove_worktree(dir.path(), &wt_path).unwrap(); + } + + #[test] + fn checkpoint_commit_with_excludes_skips_modified_tracked_files() { + let dir = tempfile::tempdir().unwrap(); + init_repo(dir.path()); + create_branch(dir.path(), "excl-mod-branch").unwrap(); + + let wt_path = dir.path().join("worktree"); + add_worktree(dir.path(), &wt_path, "excl-mod-branch").unwrap(); + + // Create and commit a file in the excluded dir first + fs::create_dir_all(wt_path.join(".cache")).unwrap(); + fs::write(wt_path.join(".cache/data.bin"), "v1").unwrap(); + checkpoint_commit(&wt_path, "run", "setup", "ok", 0, None, &[]).unwrap(); + + // Now modify the tracked excluded file and add a new non-excluded file + fs::write(wt_path.join(".cache/data.bin"), "v2").unwrap(); + fs::write(wt_path.join("result.txt"), "done").unwrap(); + + let excludes = vec!["**/.cache/**".to_string()]; + checkpoint_commit(&wt_path, "run", "step", "ok", 1, None, &excludes).unwrap(); + + let output = Command::new("git") + .args(["show", "--name-only", "--format=", "HEAD"]) + .current_dir(&wt_path) + .output() + .unwrap(); + let committed_files = String::from_utf8_lossy(&output.stdout); + assert!( + committed_files.contains("result.txt"), + "result.txt should be committed" + ); + assert!( + !committed_files.contains(".cache"), + ".cache should be excluded" + ); + + remove_worktree(dir.path(), &wt_path).unwrap(); + } } diff --git a/crates/arc-workflows/src/handler/manager_loop.rs b/crates/arc-workflows/src/handler/manager_loop.rs index c1a12fc6a..342fd9064 100644 --- a/crates/arc-workflows/src/handler/manager_loop.rs +++ b/crates/arc-workflows/src/handler/manager_loop.rs @@ -147,6 +147,7 @@ impl Handler for SubWorkflowHandler { run_branch: None, meta_branch: None, labels: HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; // Clone parent context for child; inject parent preamble diff --git a/crates/arc-workflows/src/handler/parallel.rs b/crates/arc-workflows/src/handler/parallel.rs index d5d9a1818..b99e6b062 100644 --- a/crates/arc-workflows/src/handler/parallel.rs +++ b/crates/arc-workflows/src/handler/parallel.rs @@ -243,6 +243,7 @@ impl Handler for ParallelHandler { "parallel_base".into(), 0, None, + gs.checkpoint_exclude_globs.clone(), ) .await } @@ -254,6 +255,7 @@ impl Handler for ParallelHandler { "parallel_base", 0, None, + &gs.checkpoint_exclude_globs, ) .await } diff --git a/crates/arc-workflows/tests/daytona_integration.rs b/crates/arc-workflows/tests/daytona_integration.rs index 4ce7aef4f..bc378892c 100644 --- a/crates/arc-workflows/tests/daytona_integration.rs +++ b/crates/arc-workflows/tests/daytona_integration.rs @@ -331,6 +331,7 @@ async fn daytona_pipeline_artifact_offload_and_sync() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -522,6 +523,7 @@ async fn daytona_git_checkpoint_remote_emits_events() { run_branch: Some(branch_name), meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -702,6 +704,7 @@ async fn daytona_parallel_git_branching_e2e() { run_branch: Some(branch_name), meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -1027,6 +1030,7 @@ async fn daytona_git_checkpoint_with_shadow_branch() { run_branch: Some(branch_name), meta_branch: Some(meta_branch), labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -1164,6 +1168,7 @@ async fn daytona_asset_collection() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -1394,6 +1399,7 @@ async fn daytona_git_push_run_branch_to_origin() { run_branch: Some(branch_name.clone()), meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine diff --git a/crates/arc-workflows/tests/integration.rs b/crates/arc-workflows/tests/integration.rs index aa52f10ca..c69f7b0ad 100644 --- a/crates/arc-workflows/tests/integration.rs +++ b/crates/arc-workflows/tests/integration.rs @@ -199,6 +199,7 @@ async fn end_to_end_linear_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -336,6 +337,7 @@ async fn end_to_end_branching_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -457,6 +459,7 @@ async fn end_to_end_human_gate_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -566,6 +569,7 @@ async fn goal_gate_routes_to_retry_target_on_failure() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -685,6 +689,7 @@ async fn goal_gate_routes_to_retry_target_when_present() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -991,6 +996,7 @@ async fn retry_on_failure_then_succeed() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -1064,6 +1070,7 @@ async fn pipeline_with_many_nodes() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -1385,6 +1392,7 @@ async fn smoke_test_with_mock_codergen_backend() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -1484,6 +1492,7 @@ async fn end_to_end_parallel_fan_out_fan_in() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -1594,6 +1603,7 @@ async fn resume_from_checkpoint_completes_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -1690,6 +1700,7 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; // This should succeed because goal gate for gated_work is satisfied @@ -1731,6 +1742,7 @@ async fn graph_goal_in_context() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -1764,6 +1776,7 @@ async fn event_streaming_lifecycle() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -1841,6 +1854,7 @@ async fn context_flow_between_stages() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -1891,6 +1905,7 @@ async fn tool_handler_e2e() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -1960,6 +1975,7 @@ async fn auto_approve_interviewer_e2e() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -1994,6 +2010,7 @@ async fn codergen_without_backend_simulated() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -2096,6 +2113,7 @@ async fn branching_loop_back_on_failure() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2180,6 +2198,7 @@ async fn human_gate_loops_back() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2235,6 +2254,7 @@ async fn scenario_ship_a_feature() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2318,6 +2338,7 @@ async fn scenario_parallel_expert_review() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2395,6 +2416,7 @@ async fn scenario_node_retries_on_retry_status() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2454,6 +2476,7 @@ async fn scenario_loop_restart_resets_context() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2519,6 +2542,7 @@ async fn scenario_bug_triage_router() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2574,6 +2598,7 @@ async fn scenario_crash_recovery() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -2680,6 +2705,7 @@ async fn manager_loop_stop_condition_satisfied_e2e() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); @@ -2754,6 +2780,7 @@ async fn manager_loop_max_cycles_exceeded_e2e() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); @@ -2887,6 +2914,7 @@ async fn conditional_branching_success_fail_paths() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2937,6 +2965,7 @@ async fn edge_selection_condition_match_wins_over_weight() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -2981,6 +3010,7 @@ async fn edge_selection_weight_breaks_ties() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -3017,6 +3047,7 @@ async fn edge_selection_lexical_tiebreak() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -3072,6 +3103,7 @@ async fn context_updates_visible_across_nodes() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -3113,6 +3145,7 @@ async fn stylesheet_applies_model_override() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -3166,6 +3199,7 @@ async fn custom_handler_registration_and_execution() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -3234,6 +3268,7 @@ async fn integration_smoke_plan_implement_review_done() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -3335,6 +3370,7 @@ async fn manager_loop_runs_child_engine_e2e() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -3467,6 +3503,7 @@ async fn manager_loop_context_flows_e2e() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); @@ -3538,6 +3575,7 @@ async fn manager_loop_child_dotfile_e2e() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); @@ -3649,6 +3687,7 @@ async fn graph_merge_e2e_through_engine() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -3797,6 +3836,7 @@ async fn fidelity_default_is_compact() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -3851,6 +3891,7 @@ async fn fidelity_graph_default_applied() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -3901,6 +3942,7 @@ async fn fidelity_node_overrides_graph_default() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -3957,6 +3999,7 @@ async fn fidelity_edge_overrides_node_and_graph() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4003,6 +4046,7 @@ async fn fidelity_full_produces_empty_preamble() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4059,6 +4103,7 @@ async fn fidelity_truncate_preamble_minimal() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4128,6 +4173,7 @@ async fn fidelity_summary_low_mode() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4192,6 +4238,7 @@ async fn fidelity_summary_medium_mode() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4256,6 +4303,7 @@ async fn fidelity_summary_high_mode() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4313,6 +4361,7 @@ async fn fidelity_full_sets_thread_id_in_context() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4381,6 +4430,7 @@ async fn fidelity_full_nodes_share_thread_id() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4458,6 +4508,7 @@ async fn fidelity_resume_degrades_full_to_summary_high() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -4551,6 +4602,7 @@ async fn fidelity_resume_degrade_only_affects_first_hop() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -4631,6 +4683,7 @@ async fn fidelity_resume_no_degrade_when_not_full() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -4670,6 +4723,7 @@ async fn fidelity_stored_in_checkpoint_context() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4753,6 +4807,7 @@ async fn fidelity_precedence_multi_node_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4818,6 +4873,7 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -4891,6 +4947,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine_low .run(&graph_low, &config_low) @@ -4956,6 +5013,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine_med .run(&graph_med, &config_med) @@ -5024,6 +5082,7 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -5075,6 +5134,7 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -5129,6 +5189,7 @@ async fn fidelity_edge_thread_id_override_in_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -5184,6 +5245,7 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -5249,6 +5311,7 @@ async fn fidelity_from_parsed_dot_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -5294,6 +5357,7 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -5361,6 +5425,7 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine.run(&graph, &config).await.expect("run"); @@ -5444,6 +5509,7 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -5635,6 +5701,7 @@ mod real_llm { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = tokio::time::timeout( @@ -5747,6 +5814,7 @@ mod real_llm { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = tokio::time::timeout( @@ -5886,6 +5954,7 @@ mod real_llm { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = tokio::time::timeout( @@ -5991,6 +6060,7 @@ mod real_llm { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = tokio::time::timeout( @@ -6087,6 +6157,7 @@ async fn human_gate_freeform_only_routes_text() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -6218,6 +6289,7 @@ async fn human_gate_freeform_with_fixed_choice_match() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -6333,6 +6405,7 @@ async fn human_gate_freeform_fallback_on_unmatched_text() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -6462,6 +6535,7 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -6571,6 +6645,7 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -6828,6 +6903,7 @@ fn make_run_config(dir: &std::path::Path) -> RunConfig { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), } } @@ -7919,6 +7995,7 @@ async fn arc_e2e_with_real_llm() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -8045,6 +8122,7 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; engine @@ -8242,6 +8320,7 @@ async fn large_context_values_are_offloaded_to_artifact_store() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -8452,6 +8531,7 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -8580,6 +8660,7 @@ async fn node_dir_uses_visit_count_on_revisit() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -9496,6 +9577,7 @@ async fn full_pipeline_with_cli_backend_node() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -9623,6 +9705,7 @@ async fn stylesheet_backend_property_routes_to_cli() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -9902,6 +9985,7 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() { run_branch: Some("arc/run/test-docker".to_string()), meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; // 5. Run pipeline @@ -10085,6 +10169,7 @@ async fn git_checkpoint_host_writes_shadow_branch() { run_branch: Some(format!("arc/run/{run_id}")), meta_branch: Some(meta_branch), labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; // 5. Run pipeline @@ -10277,6 +10362,7 @@ async fn parallel_git_branching_host_e2e() { run_branch: Some(run_branch.clone()), meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; // 5. Run pipeline @@ -10537,6 +10623,7 @@ async fn git_checkpoint_host_skips_empty_diff_patch() { run_branch: Some("arc/run/empty-diff".to_string()), meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -10917,6 +11004,7 @@ async fn e2e_circuit_breaker_deterministic_self_loop() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -10962,6 +11050,7 @@ async fn e2e_circuit_breaker_custom_limit() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11000,6 +11089,7 @@ async fn e2e_circuit_breaker_ignores_transient_failures() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11045,6 +11135,7 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11083,6 +11174,7 @@ async fn e2e_circuit_breaker_loop_restart() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11143,6 +11235,7 @@ async fn e2e_failure_signature_persisted_in_context() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.unwrap(); @@ -11205,6 +11298,7 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let _outcome = engine.run(&graph, &config).await.unwrap(); @@ -11259,6 +11353,7 @@ async fn e2e_signature_maps_persist_in_checkpoint() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.unwrap(); @@ -11383,6 +11478,7 @@ async fn e2e_circuit_breaker_emits_events_before_abort() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11448,6 +11544,7 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.unwrap(); @@ -11542,6 +11639,7 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11636,6 +11734,7 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11674,6 +11773,7 @@ async fn e2e_loop_restart_blocked_for_structural_failure() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11712,6 +11812,7 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11750,6 +11851,7 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11785,6 +11887,7 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11824,6 +11927,7 @@ async fn e2e_loop_restart_allowed_for_transient_infra() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11926,6 +12030,7 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let result = engine.run(&graph, &config).await; @@ -11980,6 +12085,7 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -12024,6 +12130,7 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -12087,6 +12194,7 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let start = std::time::Instant::now(); @@ -12215,6 +12323,7 @@ async fn asset_collection_local_sandbox_success() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -12321,6 +12430,7 @@ async fn asset_collection_local_sandbox_on_failure() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -12410,6 +12520,7 @@ async fn asset_collection_docker_sandbox() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine @@ -12477,6 +12588,7 @@ async fn wait_timer_e2e() { run_branch: None, meta_branch: None, labels: std::collections::HashMap::new(), + checkpoint_exclude_globs: Vec::new(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); diff --git a/docker/demo-server.toml b/docker/demo-server.toml index 26a440c6a..8b0b8cf29 100644 --- a/docker/demo-server.toml +++ b/docker/demo-server.toml @@ -3,3 +3,17 @@ base_url = "http://api:3000" [feature_flags] session_sandboxes = false + +[checkpoint] +exclude_globs = [ + "**/node_modules/**", + "**/.pnpm-store/**", + "**/.npm/**", + "**/.cache/**", + "**/playwright-report/**", + "**/test-results/**", + "**/.cargo-target*/**", + "**/.cargo_target*/**", + "**/.wasm-pack/**", + "**/.tmpbuild/**", +]